1. Create your Quarkus Project

you find this project in the labs-folder
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)

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
output
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
check the result
ll -lah *.pem
output
-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

2.3. Move Files into Resource Folder

mv privateKey.pem ./src/main/resources
mv publicKey.pem ./src/main/resources/META-INF/resources
rm rsaPrivateKey.pem  (1)
1 remove the rsa key - it is not needed
jwt 01 folder

2.4. Set Properties

mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem
mp.jwt.verify.issuer=htl-leonding
quarkus.smallrye-jwt.enabled=true

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);
    }

}

4. Add the TokenService

5. Add the Endpoint (Resource)

UserResource.java
Unresolved directive in jwt-rbac.adoc - include::../labs/openid-connect-policies/src/main/java/at/htl/UserResource.java[]

6. Application configuration

application.properties
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 .

8. Add a extension

./mvnw quarkus:add-extension -Dextensions="keycloak-authorization"

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.

  1. Create a folder called http-request in the project root.

  2. Create a file called requests.http (the http-ending is important)

  3. Open the file requests.http and click on Add Environmental File → Option Regular

Now a file called http-client.env.json was created:

We define some variables:

http-client.env.json
{
  "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
requests.http
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

clients authorization resources

  • 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

clients authorization permissions

13. Test the Access to the Resources

13.1. Resource admin is unauthorized (w/o any authorization)

Request with variable
GET {{quarkus-host}}/api/admin
Request + Response
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)

Request with variable
GET {{quarkus-host}}/api/users
Request + Response
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

14.2. Accesses the users-Resource - 200

GET {{quarkus-host}}/api/users
Authorization: Bearer {{auth_token}}

14.3. Accesses the admin-Resource - 403

Request with variable
GET {{quarkus-host}}/api/admin
Authorization: Bearer {{auth_token}}
Request + Response
GET http://localhost:8080/api/admin

HTTP/1.1 403 Forbidden
content-length: 0

<Response body is empty>

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

15.2. Accesses the users-Resource - 200

GET {{quarkus-host}}/api/users
Authorization: Bearer {{auth_token}}

15.3. Accesses the admin-Resource - 200

Request with variable
GET {{quarkus-host}}/api/admin
Authorization: Bearer {{auth_token}}
Request + Response
GET http://localhost:8080/api/admin

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Length: 10
Content-Type: application/json

I am admin