Debugging Keycloak OIDC token exchange

March 22, 2023 

Introduction

There's no substitute for understanding what you're doing, and that in turn is difficult without seeing what is happening. Debugging Keycloak OIDC problems without understanding what is happening under the hood is no exception to this rule. The purpose of this article is twofold:

  1. Help you learn how Keycloak OIDC token exchange work
  2. Provide you with the tools to debug Keycloak OIDC token exchanges in production environments

For the purposes of this blog post I've been using the OpenID Connect Playground application from the book Keycloak - Identity and Access Management for Modern Applications from Packt Publishing. I can recommend that book for anyone who needs to understand Keycloak and in particular the protocols it supports (OAuth2, OIDC and SAML 2.0).

Debugging Keycloak OIDC token exchange with tcpdump

Doing the packet capture

If traffic between Keycloak and the client (e.g. a web application) is not encrypted, debugging Keycloak OIDC token exchanges is easy to do with tcpdump. Here's a sample tcpdump command-line that ran on the computer running the web browser:

$ tcpdump -i vboxnet1 port 8080 -w /tmp/keycloak.pcap

The "-i" option defines the network interface to capture traffic on. Once you have tcpdump listening do whatever is failing and then read the packet capture in ASCII mode:

$ tcpdump -A -r /tmp/keycloak.pcap

To get all possible data you need to run tcpdump at both sides - browser and Keycloak.

Example Authorization Code flow exchange between browser and Keycloak

Below you'll see the exchange between the application and Keycloak that use Authorization Code flow, which is essentially the same as Authorization Code grant type in OAuth2. Her the Keycloak server lives at http://192.168.56.80:8080. I recorded the flow below from the computer running the browser. To get the full token exchange you also have to record from the Keycloak side.

Here the browser asks Keycloak realm's OpenID Connect authorization endpoint (/auth/realms/master/protocol/openid-connect/auth) for an authorization code:

13:03:16.666614 IP laptop.56044 > keycloak.webcache: Flags [P.], seq 1:1338, ack 1, win 502, options [nop,nop,TS val 3674982084 ecr 1686713802], length 1337: HTTP: GET /auth/realms/master/protocol/openid-connect/auth?client_id=oidc-playground&response_type=code&redirect_uri=http://localhost:8000/&scope=openid&prompt=login&max_age=3600&login_hint=oidc-playground HTTP/1.1

The interesting parameters are:

  • client_id: the ID of the Keycloak client the (web) application
  • response_type: the type of response the application is expecting. In this case an authorization code (as implied by Authorization Code flow).
  • redirect_uri: where to redirect the browser after successful authentication in Keycloak, i.e. the web application's URL. You must have this in the Redirect URIs list of the Keycloak client.
  • login_hint: the name of the user to pass to Keycloak. Keycloak automatically fills the username filed with this value, if present.
  • max_age: how long will the the authentication considered valid.

After successful authentication Keycloak sends the browser an authorization code (see code below):

13:03:20.136822 IP keycloak.webcache > laptop.56046: Flags [P.], seq 82239:85248, ack 1700, win 501, options [nop,nop,TS val 1686717272 ecr 3674985450], length 3009: HTTP: HTTP/1.1 302 Found
--- snip ---
Location: http://localhost:8000/?session_state=f837974a-f26b-464a-8a2c-d70f0886c7a1&code=84334b87-fa5d-46e2-b43c-53f483b33db7.f837974a-f26b-464a-8a2c-d70f0886c7a1.6f46e3e3-5421-4439-995a-dfa6e7710141
--- snip ---

The laptop then sends the authorization code to the realm's token endpoint. Note how the code is the same as above (84334b87...):

13:03:24.644215 IP laptop.56042 > keycloak.webcache: Flags [P.], seq 379:1032, ack 6170, win 501, options [nop,nop,TS val 3674990062 ecr 1686717952], length 653: HTTP: POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
--- snip ---
grant_type=authorization_code&code=84334b87-fa5d-46e2-b43c-53f483b33db7.f837974a-f26b-464a-8a2c-d70f0886c7a1.6f46e3e3-5421-4439-995a-dfa6e7710141&client_id=oidc-playground&redirect_uri=http://localhost:8000/

Keycloak sends an access token, ID token and refresh token back to the browser:

13:03:24.655139 IP keycloak.webcache > laptop.56042: Flags [P.], seq 6170:10045, ack 1032, win 502, options [nop,nop,TS val 1686721790 ecr 3674990062], length 3875: HTTP: HTTP/1.1 200 OK
--- snip ---
{
  "access_token":"<long string>",
  "expires_in":60,
  "refresh_expires_in":1800,
  "refresh_token":"<long string>",
  "token_type":"Bearer",
  "id_token":"<long string>",
  "not-before-policy":0,
  "session_state":"f837974a-f26b-464a-8a2c-d70f0886c7a1",
  "scope":"openid email profile"
}

All the tokens are JSON Web Tokens (JWT) and consist of three dot-separated parts. The first part is a base64-encoded header and the second part is the base64-encoded payload. The third part is the signature. You can simply copy the strings from tcpdump, split at "." and base64 decode the segments to get the JSON-formatted data, as shown below. In JWT tokens all times are in the Unix epoch time. RFC 7519 documents the fields present in these tokens so I won't go through them here.

The access token

The access token contains, among other things, the level of access Keycloak granted the user. The header is very boring:

$ echo "<access-token>"|cut -d "." -f 1|base64 -d
{
  "alg":"RS256",
  "typ" : "JWT",
  "kid" : "pFa6nY7UzYpsxBnNNve8yzwu1LpxdcAmMiqIusSThKg"
}

The payload is the more interesting part:

$ echo "<access-token>"|cut -d "." -f 2|base64 -d
{
  "exp":1679483071,
  "iat":1679483011,
  "auth_time":1679483000,
  "jti":"34b23679-d670-4002-b35c-71071b0ab219",
  "iss":"http://192.168.56.80:8080/auth/realms/master",
  "aud":"account",
  "sub":"1356f09b-dd9c-4f72-aeb5-c86351478c15",
  "typ":"Bearer",
  "azp":"oidc-playground",
  "session_state":"f837974a-f26b-464a-8a2c-d70f0886c7a1",
  "acr":"1",
  "allowed-origins":["http://localhost:8000"],
  "realm_access":{"roles":["default-roles-master","offline_access","uma_authorization"]},
  "resource_access":{"account":{"roles":["manage-account","manage-account-links","view-profile"]}},
  "scope":"openid email profile",
  "sid":"f837974a-f26b-464a-8a2c-d70f0886c7a1",
  "email_verified":false,
  "preferred_username":"oidc-playground"
}

The ID token

As with access tokens the header is very boring:

$ echo <id-token>|cut -d "." -f 1|base64 -d
{
  "alg":"RS256",
  "typ" : "JWT",
  "kid" : "pFa6nY7UzYpsxBnNNve8yzwu1LpxdcAmMiqIusSThKg"
}

The payload:

$ echo <id-token>|cut -d "." -f 2|base64 -d
{
  "exp":1679483064,
  "iat":1679483004,
  "auth_time":1679483000,
  "jti":"80ff10a3-9f3b-4f44-bb57-d5612699e40a",
  "iss":"http://192.168.56.80:8080/auth/realms/master",
  "aud":"oidc-playground",
  "sub":"1356f09b-dd9c-4f72-aeb5-c86351478c15",
  "typ":"ID",
  "azp":"oidc-playground",
  "session_state":"f837974a-f26b-464a-8a2c-d70f0886c7a1",
  "at_hash":"KSQeRU72hKCui4V5b1Pnew",
  "acr":"1",
  "sid":"f837974a-f26b-464a-8a2c-d70f0886c7a1",
  "email_verified":false,
  "preferred_username":"oidc-playground"
}

Much of the data in the ID token is derived from the Keycloak user you authenticated as. For example, if your user has an email address, first name and last name, you will see additional fields like these:

  "name": "Foo Bar",
  "given_name": "Foo",
  "family_name": "Bar",
  "email": "[email protected]",

The refresh token

The header:

$ echo <refresh-token>|cut -d "." -f 1|base64 -d
{
  "alg":"HS256",
  "typ" : "JWT",
  "kid" : "cd7e1f08-7d77-44d8-bb8e-33353afe666f"
}

The payload:

{
  "exp":1679484811,
  "iat":1679483011,
  "jti":"e506bb8f-379a-4c9a-affd-fa5407a7263f",
  "iss":"http://192.168.56.80:8080/auth/realms/master",
  "aud":"http://192.168.56.80:8080/auth/realms/master",
  "sub":"1356f09b-dd9c-4f72-aeb5-c86351478c15",
  "typ":"Refresh",
  "azp":"oidc-playground",
  "session_state":"f837974a-f26b-464a-8a2c-d70f0886c7a1",
  "scope":"openid email profile",
  "sid":"f837974a-f26b-464a-8a2c-d70f0886c7a1"
}

Debugging Keycloak OIDC token exchange with OAuth 2.0 tracer in Google Chrome

If you're only interested in what the browser sees you can use the SAML, WS-Federation and OAuth 2.0 tracer extension to Google Chrome. I suggest you to turn on verbosity to the maximum in options. Once you start recording debugging Keycloak OIDC token exchange is just a matter of checking the messages in the traces. That is way easier than parsing the raw tcpdump data. Also, if traffic between Keycloak and browser is end-to-end encrypted with HTTPS then tcpdump might not be an option at all.

Debugging Keycloak OIDC token exchange with Firefox DevTools

Firefox DevTools, which are built-in in to Firefox, allow you to view HTTP requests and payloads. Press Ctrl-Shift-E to go straight to the Network section where you can analyze what the browser sees during token exchanges.

Evaluating client scopes

Keycloak provides tools for evaluating the tokens granted for clients (e.g. web applications). Go to the Keycloak client of your application, then select "Client Scopes", select a "User" to impersonate and click on "Evaluate". Several interesting tabs now appear:

  • Effective Protocol Mappers
  • Effective Role Scope Mappings
  • Generated Access Token
  • Generate ID Token
  • Generated User Info

As you can see, you can debug token contents directly from Keycloak, without you having to trigger a real OpenID Connect token exchange and debug the tokens that way. Similarly, you can check if there are any inconsistencies with the generated ID token and User Info, should your application prefer getting its user information from one over the other.

Learning with OpenID Connect Playground

OpenID Connect Playground is a very simple Node.js application with the sole purpose of making the OIDC token exchange visible. In OpenID Connect terms it is a Relying Party (RP) that uses the Authorization Code flow. It is a quite useful tool for learning what happens under the hood in OpenID Connect, but a debug tool it is not. However, it is a good tool for understanding how token exchange should work, so that you can more easily spot anomalies in real life.

Installing the playground

To use OpenID Connect Playground the first step is to Git clone the code:

git clone https://github.com/PacktPublishing/Keycloak-Identity-and-Access-Management-for-Modern-Applications.git

Next ensure that you npm installed. Then run

npm install
npm start

The application should now be running at http://localhost:8000.

Setting up Keycloak and Keycloak client

The OpenID Connect Playground is useless by itself, because it relies on Keycloak. For testing purposes we tend to use the Vagrant + Virtualbox environment in puppet-module-keycloak. However, running Keycloak locally or inside a container will work equally well.

Once you have Keycloak up and running, you need to create a Keycloak client to a realm (e.g. master) with the following settings:

  • Client ID: oidc-playground
  • Access Type: public
  • Valid Redirect URIs: http://localhost:8000
  • Web Origins: http://localhost:8000

Using the playground

With the client in place you should be able to use the application. In the Discovery section set the Issuer to your Keycloak realm's URL, for example http://192.168.56.80:8080/auth/realms/master. Then click "Load OpenID Provider Configuration" and you should see a bunch of JSON data that Keycloak published about itself to the application.

From this point on you should be able to play with the Authorization Code flow:

  • Authentication: application authenticates a user with Keycloak, which in turn sends and authorization code to the application on successful authentication.
  • Token: application sends the authorization code to Keycloak to request an access token, refresh token and id_token.
  • Refresh: application sends the refresh token to request a new access token
Samuli Seppänen
Samuli Seppänen
Author archive
menucross-circle