Token Types
Three tokens, three purposes
Section titled “Three tokens, three purposes”Keycloak issues three kinds of tokens during an OIDC flow. Each has a distinct audience and a distinct purpose. Mixing them up is one of the most common security mistakes in OAuth2 integrations.
Access token
Section titled “Access token”The access token authorises API calls. It is a short-lived JWT that your backend APIs validate on every request. When a client makes an API call, it attaches the access token as a Bearer token in the Authorization header:
Authorization: Bearer <access_token>Key characteristics:
- Short-lived by default (5 minutes in Keycloak).
- Contains claims about the user and their granted permissions (roles, scopes).
- Intended for your APIs — not for the client application itself.
- Should be validated on every request using the realm’s public key (via JWKS).
ID token
Section titled “ID token”The ID token tells the client application who the user is. It is consumed by the client — the frontend or application — to display user information (name, email, profile picture). It is an OIDC-specific token and is only issued when the openid scope is requested.
Key characteristics:
- Contains identity claims:
sub,name,email,preferred_username, etc. - Intended for the client application only — do NOT send it to your API.
- Should not be used for authorisation decisions.
Refresh token
Section titled “Refresh token”The refresh token is longer-lived. Its sole purpose is to let the client obtain new access tokens silently — without asking the user to log in again — as long as the user’s session is still active.
Key characteristics:
- Sent only to the Keycloak token endpoint (
/protocol/openid-connect/token). - Never sent to your API.
- Invalidated when the user logs out or when the SSO session expires.
- If the
offline_accessscope is requested, Keycloak issues an offline refresh token that persists beyond the normal session.
Decoded access token example
Section titled “Decoded access token example”Here is what a typical Keycloak access token looks like after decoding:
{ "header": { "alg": "RS256", "typ": "JWT", "kid": "abc123" }, "payload": { "exp": 1719878400, "iat": 1719874800, "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "iss": "https://auth.example.com/realms/my-app", "aud": "account", "sub": "user-uuid-here", "typ": "Bearer", "azp": "my-app-frontend", "scope": "openid profile email roles", "realm_access": { "roles": ["offline_access", "uma_authorization", "user"] }, "resource_access": { "my-app-frontend": { "roles": ["app-user"] } }, "email_verified": true, "name": "Alice Example", "preferred_username": "alice", }}{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "abc123"
},
"payload": {
"exp": 1719878400,
"iat": 1719874800,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"iss": "https://auth.example.com/realms/my-app",
"aud": "account",
"sub": "user-uuid-here",
"typ": "Bearer",
"azp": "my-app-frontend",
"scope": "openid profile email roles",
"realm_access": {
"roles": ["offline_access", "uma_authorization", "user"]
},
"resource_access": {
"my-app-frontend": {
"roles": ["app-user"]
}
},
"email_verified": true,
"name": "Alice Example",
"preferred_username": "alice",
"email": "[email protected]"
}
}Key claims explained
Section titled “Key claims explained”| Claim | Meaning |
|---|---|
sub | Subject — the unique identifier of the user (a UUID). Never changes, even if the username or email changes. |
iss | Issuer — the URL of the Keycloak realm that issued the token. Your API must verify this matches the expected realm. |
aud | Audience — which party the token is intended for. Your API should verify its own client ID appears here. |
exp | Expiry — a Unix timestamp after which the token must be rejected. |
azp | Authorised party — the client ID of the application that requested the token. |
scope | Space-separated list of granted scopes. Reflects which client scopes were included. |
realm_access | An object containing a roles array of the user’s realm-level roles. |