| Examples | Keycloak 26.7.3 decoded token payloads and token-endpoint checks |
|---|---|
| Applies to | OAuth 2.0 and OpenID Connect; examples use Keycloak |
| Scope | Who consumes each token, typical claims, destinations, lifetimes, Keycloak JWT examples, and common misuse errors. Does not cover full Authorization Code + PKCE setup, client registration, or JWT signature validation libraries. |
| Related guides | OAuth vs OIDC vs SAML Authentication vs authorization What is single sign-on (SSO)? What is Keycloak? Keycloak clients explained |
After an OIDC login, an application may receive several tokens that look similar but serve different purposes. Send the wrong one to an API and you get audience errors or silent security mistakes. OAuth authorizes API calls; OIDC adds a standard identity statement for the client. For the wider protocol chooser, see OAuth 2.0 versus OpenID Connect.
Access Token vs ID Token vs Refresh Token: Quick Comparison
The easiest way to tell the tokens apart is by their intended recipient: the API, the client, or the authorization server.
If you are new to OAuth and OpenID Connect, think of the three tokens as having three separate jobs. An ID token tells the application who signed in, an access token lets the application access an API, and a refresh token lets the application ask the authorization server for a new access token without making the user sign in again. They may all look like long strings, but they are meant for different recipients and should not be used interchangeably.
| Access token | ID token | Refresh token | |
|---|---|---|---|
| Primary job | Call a protected API or resource | Prove the user authenticated to the client | Get new tokens from the authorization server |
| Typical consumer | Resource server / API | Relying party / OIDC client | Authorization server (Keycloak token endpoint) |
| Validated by | Resource server / API | OIDC client | Authorization server / token endpoint |
| Sent to | Authorization: Bearer … on the API |
Client app only (validate locally) | Token endpoint with grant_type=refresh_token |
| Format | Opaque string or JWT | Always a JWT in OIDC | Opaque or JWT (implementation-specific) |
| Intended audience / recipient | Resource server / API | Client ID (must be included in aud) |
Authorization server / token endpoint |
| User / profile data | May contain user claims, scopes and roles | Contains authentication claims; profile claims depend on requested scopes / configuration | Applications should not rely on its claims |
| Lifetime | Short (minutes) | Short (often matches access) | Longer; treat as a secret |
A successful OpenID Connect authentication request includes an ID token when openid is requested. An access token comes from the underlying OAuth flow, while issuance of a refresh token depends on the flow and server or client configuration. A later refresh response may also omit id_token.
What Is an Access Token?
The access token is the credential your client presents to a protected resource. In HTTP that usually means:
Authorization: Bearer <access_token>The API validates that credential and then uses its scopes, roles, audience, and other policy inputs to decide whether the request is allowed. That pairs with authentication versus authorization.
What an access token typically carries when it is a JWT (as in Keycloak):
iss: realm issuer URLaud: API or resource identifiers that may accept the tokenazp: authorized party (the client that obtained the token); optional in general, common in Keycloakexp/iat: expiry and issued-at timesscope: granted scopesrealm_access/resource_access: roles Keycloak mapped into the token- Optional profile claims when client scopes include them
Do not send the access token to a third-party site that is not your API, and do not log full bearer tokens in production.
What Is an ID Token?
The ID token is OpenID Connect’s identity statement. It is always a JWT. It tells your client application who signed in and how, not which API endpoint to call.
The client validates the ID token locally:
- Signature against the realm JWKS
issmatches the expected Keycloak realmaudcontains the expected client ID (audmay be a string or an array)expis still in the futurenonce, when used in the authorization request, must match the original value; validateat_hashwhen required or applicable for the flow
Typical ID-token claims:
sub: stable subject identifier for the user in this realmaud: must include the OIDC client ID (the relying party)azp: authorized party; optional in OIDC, commonly present in Keycloak tokenspreferred_username,email,name: profile claims from requested scopesacr: authentication context class (how strong the login was)at_hash: hash binding to the access token in some flows
Authorization: Bearer on your business API. The ID token’s audience is the client, not the API. On this Keycloak lab, calling UserInfo with the ID token as a bearer returned HTTP 401. Use the access token for APIs and UserInfo.
The client validates the ID token to establish the authenticated user's identity and may then create its own application session. That fits common SSO designs, but session handling itself is application-specific. The ID token stays with the client; it is not an API bearer credential.
What Is a Refresh Token?
The refresh token goes back to Keycloak’s token endpoint, not to your API.
When the access token expires, the client posts something like:
POST /realms/{realm}/protocol/openid-connect/token
grant_type=refresh_token
client_id=…
refresh_token=…Keycloak returns a new access token (and often a new ID token and refresh token). A refresh token is typically longer-lived than an access token and must be treated as a secret. Store it like a password: server-side session store, secure cookie, or OS-backed storage on mobile. Do not put it in localStorage on a public web page if you can avoid it.
What refresh tokens are for:
- Obtain new access tokens without another interactive login while the refresh token and its underlying Keycloak session remain valid
What they are not for:
- Calling your API as a bearer credential
- Reading as a user-profile document (even when Keycloak encodes them as JWTs)
Keycloak often issues refresh tokens as JWTs (typ: Refresh). Other authorization servers issue opaque refresh tokens. Treat the format as an implementation detail. Your app should only send the refresh token to the token endpoint.
Access Token vs ID Token: Keycloak JWT Example
For payload inspection only, this isolated lab used Keycloak’s Direct Access Grant to obtain tokens without reproducing a browser flow. New applications should use Authorization Code flow with PKCE rather than using Direct Access Grant as their login design.
On a Keycloak golinuxcloud realm with public client orders-api and user tokenuser, a token response with scope=openid profile email returned JWT access and ID tokens. The payloads below are sanitized (identifiers shortened, jti / sid removed). The JWT header and payload are Base64url-encoded, not encrypted, so anyone holding these signed-but-unencrypted tokens can read their claims. Integrity comes from the signature.
Access token payload (trimmed)
{
"exp": 1789834272,
"iat": 1789833972,
"iss": "http://127.0.0.1:8080/realms/golinuxcloud",
"aud": ["orders-api", "account"],
"sub": "a7ee9670-…",
"typ": "Bearer",
"azp": "orders-api",
"acr": "1",
"realm_access": {
"roles": ["employee", "default-roles-golinuxcloud", "offline_access", "uma_authorization"]
},
"resource_access": {
"orders-api": { "roles": ["order-reader"] }
},
"scope": "openid lab-claims profile email",
"preferred_username": "tokenuser",
"email": "tokenuser@golinuxcloud.lab",
"name": "Token User"
}ID token payload (trimmed)
{
"exp": 1789834272,
"iat": 1789833972,
"iss": "http://127.0.0.1:8080/realms/golinuxcloud",
"aud": "orders-api",
"sub": "a7ee9670-…",
"typ": "ID",
"azp": "orders-api",
"acr": "1",
"at_hash": "WBhVmz7v3ZRUC-62_d0s1Q",
"preferred_username": "tokenuser",
"email": "tokenuser@golinuxcloud.lab",
"name": "Token User"
}| Claim / property | Keycloak access token | Keycloak ID token |
|---|---|---|
typ |
Bearer |
ID |
aud |
Resource / API audience(s) | OIDC client (orders-api here) |
| Roles | Commonly present (realm_access / resource_access) |
Not present in this example |
at_hash |
No | Present here |
| Used by | API | Client |
| Sent as API Bearer | Yes | No |
Refresh token shape (lab observation)
The refresh token on this realm was also a JWT (alg: HS512, typ: Refresh) with aud set to the realm issuer URL and almost no profile claims. Do not assume every provider mirrors that. Decode refresh tokens only for debugging; production clients should treat them as opaque secrets.
Lifetimes observed on this realm for that grant:
- Access and ID tokens:
expires_in300 seconds - Refresh token:
refresh_expires_in1800 seconds
Your realm defaults and client settings can differ. Check Realm settings → Tokens in the Admin Console when you need production values.
Token Lifecycle: Where Each Token Goes
After a successful OIDC login (Authorization Code with PKCE in browsers, or another grant your client allows), Keycloak’s token response can include all three strings.
User signs in
│
▼
┌─────────────────────┐ token response ┌──────────┐
│ Keycloak (IdP / AS) │ ─────────────────────► │ OIDC │
└─────────────────────┘ │ client │
└────┬─────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
▼ ▼ ▼
ID token access token refresh token
validate │ │
authentication │ │ later
│ ▼ ▼
│ ┌───────────┐ ┌────────────────┐
└─ (client only) │ API / │ │ Keycloak │
│ UserInfo │ │ token endpoint │
└───────────┘ └────────────────┘Destination checklist:
| Token | Correct destination | Wrong destination |
|---|---|---|
| Access token | Resource API, UserInfo | Browser address bar, third-party analytics |
| ID token | Client validation code | API Authorization header |
| Refresh token | Keycloak token endpoint | Any business API |
When the access token’s exp passes, call the API again with a refreshed access token. When the refresh token expires or is revoked, send the user through interactive login again.
How Keycloak Issues and Refreshes Tokens
Keycloak acts as the OpenID Provider and authorization server. After the client completes a supported grant, the token endpoint returns a JSON body that can include:
access_tokenid_token(whenopenidwas requested)refresh_token(when the client and realm allow it)expires_in/refresh_expires_intoken_type(usuallyBearer)scope
Client configuration controls what appears. For a Keycloak client, Standard flow, Direct access grants, and Offline access change which grants and refresh behavior you get. Client scopes and protocol mappers decide which claims land in access versus ID tokens.
To inspect a JWT without pasting secrets into a website, decode the middle segment locally. Export an access token from your own lab as ACCESS_TOKEN, then run:
python3 - <<'PY'
import os, json, base64
tok = os.environ["ACCESS_TOKEN"]
payload = tok.split(".")[1]
pad = "=" * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload + pad)), indent=2))
PYThe printed JSON should match the access-token shape in the section above (typ, aud, azp, scope). Do not paste live tokens into public decoder sites.
Confirm UserInfo accepts the access token. Point Authorization at your realm UserInfo URL:
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/userinfo"{"sub":"a7ee9670-…","email_verified":true,"name":"Token User","preferred_username":"tokenuser","email":"tokenuser@golinuxcloud.lab",...}UserInfo returned the profile for tokenuser. That endpoint expects the access token, not the ID token.
Refresh without printing the secret. Keep the refresh token in an environment variable and ask only for metadata:
curl -s -X POST "http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token" \
-d "client_id=orders-api" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print({k:d[k] for k in ("token_type","expires_in","refresh_expires_in","scope") if k in d}); print("id_token" in d, "refresh_token" in d)'{'token_type': 'Bearer', 'expires_in': 300, 'refresh_expires_in': 1800, 'scope': 'openid lab-claims profile email'}
True TrueThe refresh exchange returned new tokens and kept the same short access lifetime. If the response includes a replacement refresh token, store that returned value.
Common Token Mistakes and Errors
| Symptom | Likely cause | Fix |
|---|---|---|
API returns 401 / invalid token with an ID token in Authorization |
ID token audience is the client, not the API | Send the access token as Bearer |
API returns audience / aud mismatch |
Access token aud does not include that API |
Fix client scopes, audience mappers, or the API’s expected audience |
UserInfo returns 401 with the ID token |
UserInfo expects an access token | Use the access token from the same token response |
Refresh fails with invalid_grant |
Refresh expired, revoked, or reused after rotation | Send the user through interactive login again |
| App “works” with ID token against a naive API | API never checked aud / typ |
Fix API validation; do not normalize bad client habits |
| Refresh token exposed from browser storage | Attacker may obtain new access tokens | Keep refresh tokens out of JavaScript-readable storage where possible |
The pattern behind most of these rows is the same: each token has one correct consumer. Validate iss, aud, exp, and signature on every JWT your code accepts.
Token Lifetimes, Rotation and Revocation
Short access tokens limit damage if a bearer string leaks. Longer refresh tokens avoid constant interactive logins. Set both under Realm settings → Tokens.
Practical habits:
- Keep access-token lifetime short for APIs (minutes, not days)
- If Revoke Refresh Token is enabled, Keycloak rotates the refresh token: after a successful refresh, save the new refresh token returned in the response because the used token is no longer valid. When rotation is disabled or suppressed, do not assume every refresh response contains a replacement token
- Clear locally stored tokens when the application logs out, and use the appropriate Keycloak logout or revocation flow when server-side invalidation is required
- Prefer confidential clients or a backend-for-frontend when browsers need refresh tokens
An API that validates a self-contained JWT only from its signature and claims may continue accepting it until exp, even after the user’s session changes. Short access-token lifetimes reduce that window; systems that require more immediate invalidation need an online validation or revocation strategy. Keep refresh tokens away from resource APIs and protect them more carefully because they can be exchanged for new access tokens.
Summary
Access tokens are credentials for APIs, ID tokens prove authentication to the OIDC client, and refresh tokens renew credentials at the authorization server. On Keycloak 26.x, access and ID tokens are JWTs with different typ and aud values. Refresh tokens may also be JWTs but must still only go to the token endpoint.
The main pitfall is treating every JWT as a bearer credential for your API. An ID token that looks similar to an access token still fails audience checks on a correct resource server, and UserInfo rejected the ID token as a bearer on this lab. Decode claims locally when you debug, never paste live tokens into public tools, and store refresh tokens like secrets.
When you configure clients next, set client scopes and token lifetimes so each token keeps the job described here.
References
- RFC 6749: The OAuth 2.0 Authorization Framework
- OpenID Connect Core 1.0
- RFC 7519: JSON Web Token (JWT)
- Keycloak Server Administration Guide
- Keycloak Documentation
Frequently Asked Questions
Is an access token always a JWT?
No. OAuth 2.0 allows opaque access tokens. The resource server may introspect them at the authorization server. Keycloak often issues JWT access tokens, but you should not assume every provider does the same.
Is a JWT the same as an access token?
No. JWT is a token format, while access token describes a token’s purpose. An access token can be a JWT or an opaque string. OIDC ID tokens are JWTs, while refresh-token format is implementation-specific.
Can I use an ID token to call an API?
No. The ID token’s audience is the OIDC client. APIs expect an access token as the bearer credential. Using an ID token as Authorization: Bearer fails on correctly validated APIs and failed with HTTP 401 against Keycloak UserInfo in this lab.
Does a refresh token contain user claims?
Usually not for application use. Even when Keycloak encodes a refresh token as a JWT, your app should treat it as an opaque secret and only send it to the token endpoint. Do not parse it for profile data.
Which token should the browser store?
Prefer not storing long-lived tokens in JavaScript-readable storage. Short-lived access tokens may live in memory. Refresh tokens belong in httpOnly secure cookies, a backend session, or mobile secure storage. The ID token is for the client to validate once when building its own session, not as a standing API credential.
How are these tokens different in Keycloak?
Keycloak issues an ID token as a JWT with typ set to ID and aud that includes the client ID. Access tokens are typically JWTs with typ set to Bearer and API audiences in aud, plus optional role claims. Refresh tokens return to the realm token endpoint. On this lab they were JWTs with typ set to Refresh; still treat them as opaque secrets.

