/keycloak-custom-protocol-mapper-example

An example for building custom keycloak protocol mappers

Primary LanguageJavaApache License 2.0Apache-2.0

Keycloak custom protocol mapper example / customize JWT tokens

Per default Keycloak writes a lot of things into the JWT tokens, e.g. the preferred username. If that is not enough, a lot of additional built in protocol mappers can be added to customize the JWT token created by Keycloak even further. They can be added in the client section via the mappers tab (see the documentation). But sometimes the build in protocol mappers are not enough. If this is the case, an own protocol mapper can be added to Keycloak via an (not yet) official service provider API. This project shows how this can be done.

Entrypoints into this project

  1. data-setup: Project to configure Keycloak via its REST API. Configures a realm so that it uses the example protocol mapper. Contains a main method which can be executed against a running Keycloak instance. Doesn't need to be executed manually because it's executed automatically by the docker-entrypoint.sh during startup.
  2. protocol-mapper: Contains the protocol mapper code. The resulting jar file will be deployed to Keycloak. I tried to explain things needed in comments in the protocol-mapper project
  3. Dockerfile: Adds the jar file containing the protocol mapper, created by the protocol-mapper project, to the keycloak instance.

Try it out

To try it out do the following things:

Konfiguration of keycloak

  1. If you have already started this project and changed something, execute docker-compose down -v so that the volumes and so on are destroyed. Otherwise the old keycloak in memory database might be reused or you might not see your changed data.
  2. Start build and start keycloak using docker: docker-compose up --build.
  3. After the keycloak has been started, the main class DataSetupMain in our data-setup module should be started automatically by the docker-entrypoint.sh in the Dockerfile and should add some example data to the keycloak instance. You should see the message The data has been imported in the console if it has been executed successfully.
  4. Now you can open the Keycloak admin console and login with username / password: admin / password. This initial password for the admin user were configured in our docker-compose file.
  5. You should see that the master and an example realm, which was added by the data-setup module automatically, exists currently. For this example realm the hello world mapper is configured (in clients=>example-realm-client=>Client scopes=>dedicated): Keycloak screenshot

Now Keycloak is configured. As a next step we want to check the token.

Checking the access token

To check the token, we need to login. To get the tokens using the direct flow (not recommended for production usage, just for easy demo purposes. See this page) execute the following curl command:

curl -d 'client_id=example-realm-client' -d 'username=jdoe' -d 'password=password' -d 'grant_type=password' 'http://localhost:11080/auth/realms/example-realm/protocol/openid-connect/token'

Note that using the direct flow is only possible because we configured keycloak to allow it in the RealmSetup class. Response should be like:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYbl9PXzN6VHJpSjBzOE5RUzlpMVpBcF9pZVN2YXRwOHRIWmtpTGNwM1RrIn0.eyJleHAiOjE2NzExMzMzMjMsImlhdCI6MTY3MTEzMzAyMywianRpIjoiYTcwYjA4NjQtNmI3Mi00MjljLTliMDEtZWIzNzBhMTE5YTgzIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDoxMTA4MC9hdXRoL3JlYWxtcy9leGFtcGxlLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjkxMmNkZmJhLWNlNGQtNDgzMS04NjA3LWQzM2VmOTkzOTdmYyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImV4YW1wbGUtcmVhbG0tY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImRlMzljN2M2LTM0ZWMtNGM4MC1iZTM2LWIyODE4YTkxMjMyYyIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1leGFtcGxlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6ImRlMzljN2M2LTM0ZWMtNGM4MC1iZTM2LWIyODE4YTkxMjMyYyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkpvaG4gRG9lIiwiZ3JvdXBzIjpbXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJEb2UiLCJleGFtcGxlIjp7Im1lc3NhZ2UiOiJoZWxsbyB3b3JsZCJ9fQ.wZI33cy6X2yxnsz1HeU3snrPi8xg1Pq8TiNIxPfP-RLtPQm5-3of9kTFXNvtZkA2Om3rzlI_NfyYy8eq4VArujVvvkKx5oxGZ0Q9Tv6LU0ufS4YfW0t0oAbEdNmONBXUszcl_HKX_5Pnvbs7DwR04ErAmzguECnky9hdYy0nJREnfrTwr6Ss270H8HaQ-DJ1T4x-iFzuwRkQZTg_PUfRxts0tjsIRehFPxadLujj4ZpsguvfXqCD11Gb4a2xXSm6S2iDP8sa_zwaWCbRDraBUCcEy192hADDNVDBQPYgUe-0Sj7z_mPNviEiMagAmBFCj8W-czkEWwnX_WodeVThWA",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzODE5NTdiZi1jMzI0LTQ3M2UtOTA4MS1lN2MxODVmMzllYjUifQ.eyJleHAiOjE2NzExMzQ4MjMsImlhdCI6MTY3MTEzMzAyMywianRpIjoiYWM4Njk0NzktYzY2Yi00YWIwLWIzYzQtZDc1ZjU0NWZmOTk3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDoxMTA4MC9hdXRoL3JlYWxtcy9leGFtcGxlLXJlYWxtIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDoxMTA4MC9hdXRoL3JlYWxtcy9leGFtcGxlLXJlYWxtIiwic3ViIjoiOTEyY2RmYmEtY2U0ZC00ODMxLTg2MDctZDMzZWY5OTM5N2ZjIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImV4YW1wbGUtcmVhbG0tY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImRlMzljN2M2LTM0ZWMtNGM4MC1iZTM2LWIyODE4YTkxMjMyYyIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6ImRlMzljN2M2LTM0ZWMtNGM4MC1iZTM2LWIyODE4YTkxMjMyYyJ9.AKWuXIuq__KzZC32GrGlhbDe_gZkyQsqKRSIDBKSgJQ",
  "token_type": "Bearer",
  "not-before-policy": 0,
  "session_state": "de39c7c6-34ec-4c80-be36-b2818a91232c",
  "scope": "email profile"
}

Then copy the access_token value and decode it, e.g. by using jwt.io. You'll get something like the following:

  {
     "exp": 1671133323,
     "iat": 1671133023,
     "jti": "a70b0864-6b72-429c-9b01-eb370a119a83",
     "iss": "http://localhost:11080/auth/realms/example-realm",
     "aud": "account",
     "sub": "912cdfba-ce4d-4831-8607-d33ef99397fc",
     "typ": "Bearer",
     "azp": "example-realm-client",
     "session_state": "de39c7c6-34ec-4c80-be36-b2818a91232c",
     "acr": "1",
     "realm_access": {
        "roles": [
           "default-roles-example-realm",
           "offline_access",
           "uma_authorization"
        ]
     },
     "resource_access": {
        "account": {
           "roles": [
              "manage-account",
              "manage-account-links",
              "view-profile"
           ]
        }
     },
     "scope": "email profile",
     "sid": "de39c7c6-34ec-4c80-be36-b2818a91232c",
     "email_verified": false,
     "name": "John Doe",
     "groups": [],
     "preferred_username": "jdoe",
     "given_name": "John",
     "family_name": "Doe",
     "example": {
        "message": "hello world"
     }
  }

The value auf our own Hello World Token mapper got added to the token because the message 'hello world' appears in the example.message field.

Acknowledgements

Links