1. Create your Quarkus Project
mvn io.quarkus:quarkus-maven-plugin:2.4.2.Final:create \ -DprojectGroupId=at.htl \ -DprojectArtifactId=security-jwt-rbac-tutorial \ -DclassName="at.htl.ProfileResource" \ -Dpath="/profile" \ -Dextensions="resteasy-jsonb, smallrye-jwt"
2. Create the keys
-
smallrye-jwt "… can currently verify only JWT tokens using the PEM keys …"
-
So we will use openssh and not ssh-keygen (like in other tutorials)
-
This is no problem, because openssh uses the openssl library
-
Do not change the file names of the keys, because they are referenced later in the TokenUtils-class |
2.1. Create the Private Key
openssl genrsa -out rsaPrivateKey.pem 2048
openssl pkcs8 -topk8 -inform PEM -in rsaPrivateKey.pem -out privateKey.pem -nocrypt
Generating RSA private key, 2048 bit long modulus ...........................................................................................................................................+++ ........................................+++ e is 65537 (0x10001)
2.2. Create the Public Key
openssl rsa -in privateKey.pem -out publicKey.pem -pubout -outform PEM
ll -lah *.pem
-rw-r--r-- 1 stuetz staff 1.7K Nov 14 18:08 privateKey.pem -rw-r--r-- 1 stuetz staff 451B Nov 14 18:08 publicKey.pem -rw-r--r-- 1 stuetz staff 1.6K Nov 14 18:08 rsaPrivateKey.pem
3. Add the TokenUtils
-
create the class
org.acme.jwt.utils.TokenUtils
-
copy this code into the class
Code
package org.acme.jwt.utils;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import org.eclipse.microprofile.jwt.Claims;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
/**
* Utilities for generating a JWT for testing
*/
public class TokenUtils {
private TokenUtils() {
}
public static final String ROLE_USER = "User";
public static final String ROLE_ADMIN = "Admin";
public static String generateTokenString(JwtClaims claims) throws Exception {
// Use the private key associated with the public key for a valid signature
PrivateKey pk = readPrivateKey("/privateKey.pem");
return generateTokenString(pk, "/privateKey.pem", claims);
}
private static String generateTokenString(PrivateKey privateKey, String kid, JwtClaims claims) throws Exception {
long currentTimeInSecs = currentTimeInSecs();
claims.setIssuedAt(NumericDate.fromSeconds(currentTimeInSecs));
claims.setClaim(Claims.auth_time.name(), NumericDate.fromSeconds(currentTimeInSecs));
for (Map.Entry<String, Object> entry : claims.getClaimsMap().entrySet()) {
System.out.printf("\tAdded claim: %s, value: %s\n", entry.getKey(), entry.getValue());
}
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setKey(privateKey);
jws.setKeyIdHeaderValue(kid);
jws.setHeader("typ", "JWT");
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
return jws.getCompactSerialization();
}
/**
* Read a PEM encoded private key from the classpath
*
* @param pemResName - key file resource name
* @return PrivateKey
* @throws Exception on decode failure
*/
public static PrivateKey readPrivateKey(final String pemResName) throws Exception {
InputStream contentIS = TokenUtils.class.getResourceAsStream(pemResName);
byte[] tmp = new byte[4096];
int length = contentIS.read(tmp);
return decodePrivateKey(new String(tmp, 0, length, "UTF-8"));
}
/**
* Decode a PEM encoded private key string to an RSA PrivateKey
*
* @param pemEncoded - PEM string for private key
* @return PrivateKey
* @throws Exception on decode failure
*/
public static PrivateKey decodePrivateKey(final String pemEncoded) throws Exception {
byte[] encodedBytes = toEncodedBytes(pemEncoded);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
}
private static byte[] toEncodedBytes(final String pemEncoded) {
final String normalizedPem = removeBeginEnd(pemEncoded);
return Base64.getDecoder().decode(normalizedPem);
}
private static String removeBeginEnd(String pem) {
pem = pem.replaceAll("-----BEGIN (.*)-----", "");
pem = pem.replaceAll("-----END (.*)----", "");
pem = pem.replaceAll("\r\n", "");
pem = pem.replaceAll("\n", "");
return pem.trim();
}
/**
* @return the current time in seconds since epoch
*/
public static int currentTimeInSecs() {
long currentTimeMS = System.currentTimeMillis();
return (int) (currentTimeMS / 1000);
}
}
5. Add the Endpoint (Resource)
Unresolved directive in jwt-rbac.adoc - include::../labs/openid-connect-policies/src/main/java/at/htl/UserResource.java[]
6. Application configuration
Unresolved directive in jwt-rbac.adoc - include::../labs/openid-connect-policies/src/main/java/at/htl/../../../resources/application.properties[]
6.1. Problems, when using Jackson
-
In this example, we serialize not a self-built entity class. We serialize a given interface
SecurityIdentity
.-
An error occurs because of lacking getter and setter.
-
So we have to loosen the policy for serializing this interface.
-
# these properties are necessary because jackson throws an 'InvalidDefinitionException: No serializer found for class'
# https://quarkus.io/guides/rest-json#json
# würde man json-b verwenden, wäre das nicht notwendig
quarkus.jackson.fail-on-unknown-properties=false
quarkus.jackson.fail-on-empty-beans=false
7. Make the Initial Commit
cd openid-connect-policies
git init
git add .
git commit -m "inital commit"
git remote add origin https://github.com/<your github-account>/openid-connect-policies.git
git push -u origin master
idea .
9. Start Keycloak
docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e DB_VENDOR=h2 -p 8180:8080 jboss/keycloak:15.0.2
Open in Browser: http://localhost:8180/auth
10. Configure Keycloak
→ Administration Console
-
Add realm
-
Name: quarkus
-
Create
-
-
Clients
-
Create
-
ClientID: my-backend-service
-
Client Protocol: openid-connect
-
Click Save
-
-
-
Users
-
Add user
-
Details
-
Username: admin
-
Email Verified: ON
-
Save
-
-
Credentials
-
Password: passme
-
Password Confirmation: passme
-
Temporary: OFF
-
Set Password
-
Are you sure you want to set a password for the user?: Set password
-
-
-
Now make a User "user" like "admin".
-
Clients → my-backend-service
-
Settings
-
Access Type: confidential
-
Service Accounts Enabled: ON
-
Authorization Enabled: OFF
-
Valid Redirect URIs: http://localhost:8080/
-
Save
-
-
Credentials
-
Copy Secret into clipboard
-
Paste the secret in application.properties in the line "quarkus.oidc.credentials.secret"
-
-
11. Access Keycloak
Now we will access Keycloak for the first time and retrieve the access token.
-
Create a folder called
http-request
in the project root. -
Create a file called
requests.http
(the http-ending is important) -
Open the file
requests.http
and click onAdd Environmental File
→ OptionRegular
Now a file called http-client.env.json
was created:
We define some variables:
{ "dev": { "keycloak-host": "http://localhost:8180", "quarkus-host": "http://localhost:8080", "username": "my-backend-service", "password": "97fdb5b3-6fff-4090-966a-0f1c7355d0ba" (1) } }
1 | use your secret |
POST {{keycloak-host}}/auth/realms/quarkus/protocol/openid-connect/token Authorization: Basic {{username}} {{password}} Content-Type: application/x-www-form-urlencoded username=user&password=passme&grant_type=password
The output shows status code 201 and the access token.
12. Configure Resources
-
Clients → my-backend-service
-
Authorization
-
Resources
-
Actions - Delete the Default Resource → Confirm the deletion
-
Create
-
Add Resource
-
Name: Users resource
-
Display name: Users resource
-
URI: /api/users/*
-
Save
-
-
Create
-
Add Resource
-
Name: Admin resource
-
Display name: Admin resource
-
URI: /api/admin/*
-
Save
-
-
-
-
-
Roles
-
Add Role
-
Role Name: user
-
Save
-
-
Add Role
-
Role Name: admin
-
Save
-
-
-
Users
-
View all users
-
Click on ID of user 'user'
-
Role Mappings
-
add Role 'user' to Assigned Roles
-
-
-
Click on ID of user 'admin'
-
Role Mappings
-
add Roles 'user' and
admin
to Assigned Roles
-
-
-
-
Clients → my-backend-service
-
Authorization
-
Policies
-
Create Policy … → Role
-
Name: Users policy
-
Description: Ability to use users resources
-
Realm Roles: user
-
Save
-
-
Create Policy … → Role
-
Name: Admin policy
-
Description: Ability to use admins resources
-
Realm Roles: admin
-
Save
-
-
-
Permissions
-
Default Permission → Delete → Confirm Deletion
-
Create Permission… → Resource-Based
-
Name: Users permission
-
Resources: Users resource
-
Apply Policy: Users policy
-
Save
-
-
Create Permission… → Resource-Based
-
Name: Admins permission
-
Resources: Admins resource
-
Apply Policy: Admins policy
-
Save
-
-
-
-
13. Test the Access to the Resources
13.1. Resource admin is unauthorized (w/o any authorization)
GET {{quarkus-host}}/api/admin
GET http://localhost:8080/api/admin HTTP/1.1 401 Unauthorized content-length: 0 <Response body is empty> Response code: 401 (Unauthorized); Time: 644ms; Content length: 0 bytes
13.2. Resource users is unauthorized (w/o any authorization)
GET {{quarkus-host}}/api/users
GET http://localhost:8080/api/users HTTP/1.1 401 Unauthorized content-length: 0 <Response body is empty> Response code: 401 (Unauthorized); Time: 54ms; Content length: 0 bytes
14. User 'user' access Resource 'users'
14.1. Retrieves access token
POST {{keycloak-host}}/auth/realms/quarkus/protocol/openid-connect/token Authorization: Basic {{username}} {{password}} (1) Content-Type: application/x-www-form-urlencoded username=user&password=passme&grant_type=password > {% client.global.set("auth_token", response.body.access_token); %} (2)
1 | you have to provide: Authorization: Basic {client-id} {secret} |
2 | the access-token is saved in a variable auth_token , so the next request can use it |
15. User 'admin' access Resource 'users'
15.1. Retrieves access token
POST {{keycloak-host}}/auth/realms/quarkus/protocol/openid-connect/token Authorization: Basic {{username}} {{password}} (1) Content-Type: application/x-www-form-urlencoded username=admin&password=passme&grant_type=password (2) > {% client.global.set("auth_token", response.body.access_token); %}
1 | you have to provide: Authorization: Basic {client-id} {secret} |
2 | the access-token is saved in a variable auth_token , so the next request can use it |