Managing the token lifecycle in Keycloak: understanding the role of the refresh token, rotation and offline access mechanisms, token and session lifetime configuration, and the security steps for detecting token theft.

In episode 6 you used client credentials for machine-to-machine — the client simply requests a new token when the old one expires. Episode 7 is different: for users, forcing a re-login every few minutes obviously doesn't make sense. This is where the refresh token comes in — a sort of rechargeable card that allows a new access token to be obtained without the user typing their password again.
Two tokens circulate during a user's login session:
This division of roles reduces risk: if an access token leaks, the danger window only lasts its short lifetime. If a refresh token leaks, that's a big problem — because it can be used to mint new access tokens over and over.
| Aspect | Access Token | Refresh Token |
|---|---|---|
| Lifetime | Short, measured in minutes | Much longer |
| Content | Claims and roles | Session identifier |
| Usage | Sent to the target API | Only to the token endpoint |
| Validation | Resource server | Keycloak via the token endpoint |
| If stolen | Dangerous within its lifetime | Very dangerous, must be rotated |
Remember one golden rule: the refresh token never leaves the application. It stays at the client and doesn't travel with any API request.
When the access token approaches expiry, the application exchanges the refresh token at the token endpoint:
curl -X POST "https://kc.example.com/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=my-app" \
-d "client_secret=4f9a2c8d..." \
-d "refresh_token=eyJhbGciOi..."The response contains a new access token, and with rotation enabled, a new refresh token too:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"refresh_expires_in": 1800,
"token_type": "Bearer"
}grant_type=refresh_token marks this flow. Notice refresh_expires_in — the lifetime limit of the refresh token itself. As long as this value isn't zero, a further exchange remains possible.
Since recent Keycloak versions, refresh token rotation is enabled by default: every time you exchange a refresh token, Keycloak issues a new refresh token and invalidates the old one. The same old token is still accepted within a short window to tolerate request races, but outside that window, using an old token is considered a sign of theft.
This is the main defense against the stolen-token scenario: the thief and the victim compete to use the same token, and Keycloak can detect its repeated use. Don't disable rotation without a very strong reason.
For services that can't verify JWT signatures themselves, Keycloak provides introspection — asking Keycloak to assess a token's status:
curl -s -X POST "https://kc.example.com/realms/my-realm/protocol/openid-connect/token/introspect" \
-H "Authorization: Basic base64(client_id:client_secret)" \
-d "token=eyJhbGciOi..."An active: true response means the token is still valid; active: false means it has expired or been revoked. To explicitly revoke a token — e.g. when a user logs out or a refresh token is suspected stolen — use the revoke endpoint:
curl -X POST "https://kc.example.com/realms/my-realm/protocol/openid-connect/revoke" \
-d "client_id=my-app" \
-d "client_secret=4f9a2c8d..." \
-d "token=eyJhbGciOi..."Important
Remember the simple rule: access tokens can't be revoked directly. Because they're stateless JWTs, revoking them means waiting for expiry or forcing Keycloak to rotate keys. What can be revoked quickly are refresh tokens and user sessions. That's why you should treat the refresh token as the most sensitive asset in the flow.
All these numbers are set in Realm settings under the Tokens and Sessions tabs:
These three values determine a token's real lifecycle: even though a refresh token looks long-lived, it dies once its SSO session dies. Adjust to your needs — relaxed internal apps can be long, financial applications should be short.
For cases that need tokens to live longer — e.g. a mobile app opened once a month — there are two features:
offline_access scope, and Keycloak issues an offline refresh token that stays valid even after the user's SSO session expires. Its lifetime is controlled by Offline Session Idle and Offline Session Max.The difference: remember me stays tied to the user's session, while offline access lives independently. Choose deliberately — both extend credential exposure.
Several layers of defense for tokens:
A practical addition: token caching on the resource server side. Don't re-validate the signature on every request when you don't have to — cache the validation result until exp, with exceptions for actively revoked tokens.
Episode 7 explored refresh tokens: their role in extending access without re-login, the exchange flow, rotation and reuse detection, introspection and revocation, lifespan configuration in the realm, offline access and remember me, and token security strategies.
Key takeaways:
In the next episode (episode 8), you'll learn about scopes and consent — the mechanism that determines what data an application may take from a user's account, and how Keycloak presents that choice to the user on the consent screen.