Keycloak Refresh Token Rotation: Setup and Best Practices
Last updated: June 2026
Refresh token rotation issues a new refresh token every time a client exchanges an old one, and immediately invalidates the previous token. This means a stolen refresh token is only useful until the legitimate client makes its next token refresh — at which point Keycloak detects the reuse of the old token and invalidates the entire session. In Keycloak, rotation is controlled by the “Revoke Refresh Token” setting under Realm Settings > Tokens, paired with a configurable reuse count. It is strongly recommended for public clients like SPAs and mobile apps, where refresh tokens cannot be kept confidential.
Why refresh token rotation matters
Refresh tokens are long-lived credentials. Where an access token expires in minutes, a refresh token commonly lives for hours or days. That longevity is the point — it lets users stay logged in without re-authenticating — but it also makes refresh tokens a high-value target for attackers.
If an attacker intercepts or exfiltrates a refresh token (via XSS, a compromised device, or a network attack), they can silently mint new access tokens indefinitely, impersonating the user until the token expires or the user logs out. Without rotation, the legitimate user and the attacker can both hold valid refresh tokens simultaneously, and neither knows the other is there.
The OAuth 2.0 Security Best Current Practice (RFC 9700) requires sender-constrained tokens or refresh token rotation for public clients precisely because of this risk. Rotation limits the window of exploitation: the stolen token only works until the real user next hits the token endpoint, which in active sessions may be minutes.
Enabling “Revoke Refresh Token” in Keycloak
Keycloak’s rotation feature is called Revoke Refresh Token and lives in the realm-level token settings.
Admin Console path: Realm Settings > Tokens > Refresh Tokens
The relevant fields are:
| Setting | Description |
|---|---|
| Revoke Refresh Token | Master switch — enables one-time use for refresh tokens |
| Refresh Token Max Reuse | How many times a token may be reused before revocation kicks in (default: 0) |
When Revoke Refresh Token is enabled and Refresh Token Max Reuse is 0, each refresh token is strictly one-time use. On a successful /token exchange, Keycloak issues a brand-new refresh token and marks the old one as consumed. Any subsequent request presenting the consumed token triggers reuse detection.
Enabling via Admin REST API
You can also configure this programmatically using the Keycloak Admin REST API:
# Get an admin access token
ACCESS_TOKEN=$(curl -s -X POST
"https://your-keycloak.example.com/realms/master/protocol/openid-connect/token"
-d "client_id=admin-cli"
-d "username=admin"
-d "password=admin-password"
-d "grant_type=password"
| jq -r '.access_token')
# Enable Revoke Refresh Token on your realm
curl -s -X PUT
"https://your-keycloak.example.com/admin/realms/your-realm"
-H "Authorization: Bearer $ACCESS_TOKEN"
-H "Content-Type: application/json"
-d '{
"revokeRefreshToken": true,
"refreshTokenMaxReuse": 0
}'
Confirm the change:
curl -s
"https://your-keycloak.example.com/admin/realms/your-realm"
-H "Authorization: Bearer $ACCESS_TOKEN"
| jq '{revokeRefreshToken, refreshTokenMaxReuse}'
Expected output:
{
"revokeRefreshToken": true,
"refreshTokenMaxReuse": 0
}
How reuse detection works
When a client presents a refresh token at the /token endpoint, Keycloak looks up that token’s record. If Revoke Refresh Token is on, Keycloak checks whether the token has already been consumed (i.e., used more times than refreshTokenMaxReuse allows).
If the token has been used too many times, Keycloak does not simply reject the request — it invalidates the entire user session. This is the critical behavior: both the attacker’s copy and the legitimate user’s copy become worthless simultaneously. The user will need to re-authenticate.
This session termination is intentional. Because Keycloak cannot determine which party is the attacker, it forces both parties out. The legitimate user experiences a logout and signs in again; the attacker loses their foothold.
The Refresh Token Max Reuse count
Setting refreshTokenMaxReuse to 0 means strictly one-time use. A value of 1 would allow the same token to be presented twice before triggering invalidation. The only legitimate reason to use a value greater than 0 is to accommodate clients that may retry a token request after a transient network failure — though a well-implemented retry strategy (using the newly issued token from a successful response) makes this unnecessary. Keep it at 0 for maximum security.
Interaction with session timeout settings
Refresh token rotation works alongside — but does not replace — Keycloak’s session timeout hierarchy. Understanding the interaction is important for configuring realistic token lifetimes. For a detailed breakdown of SSO Session Idle, SSO Session Max, and client-level overrides, see Keycloak Session Timeout Configuration.
The relevant values for rotation are:
| Setting | Location | Effect |
|---|---|---|
| SSO Session Idle | Realm Settings > Sessions | Maximum idle time before SSO session expires |
| SSO Session Max | Realm Settings > Sessions | Hard ceiling on SSO session lifetime |
| Client Session Idle | Client > Advanced > Session settings | Client-level idle override |
| Client Session Max | Client > Advanced > Session settings | Client-level max override |
| Refresh Token Lifespan | Realm Settings > Tokens | Expiry of each individual refresh token |
A refresh token cannot outlive the SSO session it belongs to. Even with a 30-minute refresh token, if the SSO session idles out at 10 minutes, the refresh token becomes invalid. Conversely, a session that is still active will keep accepting new refresh tokens as long as they are presented within their individual expiry window.
If you are seeing unexpected token expired errors after enabling rotation, the guide to troubleshooting Keycloak token expiration covers the most common combinations.
Public clients vs. confidential clients
The risk profile differs substantially between client types, and your rotation strategy should reflect that.
Public clients (SPAs, mobile apps)
Public clients — browser-based SPAs and mobile applications — cannot securely store a client secret. Anyone who inspects the app’s source or network traffic can retrieve any credential the app holds. This makes stolen refresh tokens a realistic attack vector.
Rotation is strongly recommended for all public clients. Pair it with:
- PKCE on the authorization code flow (Keycloak enforces this by default for public clients in Keycloak 26.x)
- Short access token lifetimes (1-5 minutes)
refreshTokenMaxReuse: 0
Configure a public client for strict rotation:
# Set the client to require PKCE and confirm it is public (no secret)
curl -s -X GET
"https://your-keycloak.example.com/admin/realms/your-realm/clients?clientId=my-spa"
-H "Authorization: Bearer $ACCESS_TOKEN"
| jq '.[0] | {publicClient, attributes}'
The response should show "publicClient": true. Rotation is controlled at the realm level, so once you enable Revoke Refresh Token in realm settings, it applies to all clients in that realm, including public ones.
Confidential clients (server-side apps, backend services)
Confidential clients authenticate with a client secret, which makes credential theft harder. Refresh token theft is still possible (compromised server, leaked logs), but the risk is lower than for public clients.
Rotation is still a good practice for confidential clients and adds defense-in-depth. The operational impact is minimal because server-side code is easy to update to always store the latest refresh token.
Exception: Machine-to-machine flows using client_credentials grant do not issue refresh tokens at all — there is nothing to rotate. Rotation only applies to flows that produce refresh tokens (authorization code, device code, password — though password grant is deprecated).
Handling rotation in application code
Rotation changes the contract your application has with the token endpoint: every successful token refresh response includes a new refresh token that supersedes the old one. Your application must store it.
JavaScript / SPA (using fetch)
async function refreshTokens(currentRefreshToken) {
const response = await fetch(
'https://your-keycloak.example.com/realms/your-realm/protocol/openid-connect/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'my-spa',
refresh_token: currentRefreshToken,
}),
}
);
if (!response.ok) {
// 400 with error=invalid_grant means the token was already used
// or the session was invalidated — redirect to login
if (response.status === 400) {
redirectToLogin();
return;
}
throw new Error(`Token refresh failed: ${response.status}`);
}
const tokens = await response.json();
// Always persist the new refresh token — the old one is now invalid
sessionStorage.setItem('refresh_token', tokens.refresh_token);
sessionStorage.setItem('access_token', tokens.access_token);
return tokens;
}
The critical line is persisting tokens.refresh_token — the newly issued one — before the old token is discarded. If your app caches or logs the old value and presents it again, Keycloak will detect reuse.
Python (server-side)
import requests
def refresh_tokens(keycloak_url: str, realm: str, client_id: str,
client_secret: str, refresh_token: str) -> dict:
url = f"{keycloak_url}/realms/{realm}/protocol/openid-connect/token"
data = {
"grant_type": "refresh_token",
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
}
response = requests.post(url, data=data, timeout=10)
if response.status_code == 400:
error = response.json().get("error", "")
if error == "invalid_grant":
# Token was reused or session expired — force re-login
raise SessionExpiredError("Refresh token invalidated — user must re-authenticate")
raise ValueError(f"Token refresh error: {response.json()}")
response.raise_for_status()
new_tokens = response.json()
# Store the new refresh token — critical with rotation enabled
token_store.save(user_id, new_tokens["refresh_token"])
return new_tokens
Always handle invalid_grant explicitly. With rotation enabled this error means either the token was already used (potential replay attack detected, session invalidated) or the session timed out naturally. In both cases the correct response is to send the user through the authorization code flow again.
For a deeper treatment of token storage and validation patterns, see JWT Best Practices for Developers and the JWT token lifecycle management guide.
Offline tokens and rotation
Offline tokens are a special case. An offline token is a refresh token that persists beyond the SSO session — it allows a client to obtain new access tokens even when the user is not logged in, and it survives server restarts. They are commonly used for background jobs, mobile apps that need persistent access, and integrations that run on behalf of a user.
Offline tokens do not participate in Revoke Refresh Token rotation. The revokeRefreshToken realm setting applies only to regular (online) refresh tokens. Offline tokens are managed separately under Realm Settings > Sessions > Offline Session Idle / Offline Session Max.
For a full explanation of offline tokens, their security implications, and how to revoke them, see the Keycloak offline tokens explained guide.
If your application uses offline tokens and you are concerned about theft, the mitigation is:
- Keep offline session lifetimes as short as your use case allows
- Use the Keycloak Admin API to revoke individual offline tokens if compromise is suspected
- Require re-authentication when the offline token is first exchanged after a configurable period
Testing refresh token rotation
Before deploying rotation to production, verify the behavior in a lower environment.
Test 1: Normal rotation flow
# 1. Get initial tokens via authorization code (or resource owner password for testing)
TOKENS=$(curl -s -X POST
"https://your-keycloak.example.com/realms/your-realm/protocol/openid-connect/token"
-d "grant_type=password"
-d "client_id=test-client"
-d "username=testuser"
-d "password=testpassword")
RT1=$(echo $TOKENS | jq -r '.refresh_token')
# 2. Exchange RT1 — should get new tokens including RT2
TOKENS2=$(curl -s -X POST
"https://your-keycloak.example.com/realms/your-realm/protocol/openid-connect/token"
-d "grant_type=refresh_token"
-d "client_id=test-client"
-d "refresh_token=$RT1")
RT2=$(echo $TOKENS2 | jq -r '.refresh_token')
echo "RT1 == RT2: $([ "$RT1" = "$RT2" ] && echo YES — rotation not working || echo NO — rotation working)"
If rotation is enabled, RT1 and RT2 will be different tokens.
Test 2: Reuse detection
# After step 2 above, try using RT1 again — should fail and kill the session
REUSE_RESULT=$(curl -s -X POST
"https://your-keycloak.example.com/realms/your-realm/protocol/openid-connect/token"
-d "grant_type=refresh_token"
-d "client_id=test-client"
-d "refresh_token=$RT1")
echo $REUSE_RESULT | jq '{error, error_description}'
# Now try RT2 — should also fail because the session was invalidated
RT2_RESULT=$(curl -s -X POST
"https://your-keycloak.example.com/realms/your-realm/protocol/openid-connect/token"
-d "grant_type=refresh_token"
-d "client_id=test-client"
-d "refresh_token=$RT2")
echo $RT2_RESULT | jq '{error, error_description}'
Both should return {"error": "invalid_grant", ...}. If RT2 still works after RT1 reuse, check that revokeRefreshToken is set on the realm and not overridden at the client level.
Skycloak and rotation
If you run Keycloak on Skycloak’s managed Keycloak platform, refresh token rotation and all realm-level token settings are fully configurable through the Keycloak admin console — no infrastructure changes required. Skycloak’s session management features also give you visibility into active sessions and the ability to force-terminate sessions when a breach is suspected.
Frequently asked questions
How do I enable refresh token rotation in Keycloak?
Go to Realm Settings > Tokens in the Keycloak admin console. Enable the Revoke Refresh Token toggle and set Refresh Token Max Reuse to 0. This applies to all clients in the realm. You can also enable it via the Admin REST API by setting revokeRefreshToken: true and refreshTokenMaxReuse: 0 on the realm resource.
What is Refresh Token Max Reuse?
Refresh Token Max Reuse is a Keycloak setting that controls how many times a single refresh token can be presented at the token endpoint before Keycloak invalidates the session. A value of 0 means strictly one-time use. A value of 1 allows the same token to be used twice. Setting it above 0 is occasionally used to tolerate network retries, but 0 provides the strictest security and is recommended for most deployments.
What happens when Keycloak detects refresh token reuse?
Keycloak invalidates the entire user SSO session, not just the specific token. This means all refresh tokens and access tokens associated with that session become immediately invalid. Both the attacker and the legitimate user are logged out. The user must re-authenticate through the normal login flow. This behavior is by design — it is impossible for Keycloak to determine which party is the attacker, so it removes access for both.
Should SPAs use refresh token rotation?
Yes. The OAuth 2.0 Security Best Current Practice (RFC 9700) explicitly requires refresh token rotation for public clients, which includes all browser-based SPAs. Because SPAs cannot securely store a client secret, refresh tokens are the most sensitive long-lived credential they hold. Rotation, combined with short access token lifetimes and PKCE, substantially reduces the window of exploitation if a refresh token is stolen.
Does refresh token rotation apply to offline tokens in Keycloak?
No. The revokeRefreshToken realm setting only applies to regular (online) refresh tokens. Offline tokens are governed by separate session lifetime settings (Offline Session Idle and Offline Session Max) and must be revoked individually via the Admin API if compromised. If your use case depends heavily on offline tokens, plan a separate security strategy for them — see the Keycloak offline tokens guide for details.
How does rotation interact with Keycloak session timeouts?
Each new refresh token issued during rotation has an expiry equal to the Refresh Token Lifespan setting, but it cannot outlive the parent SSO session. If SSO Session Idle is 30 minutes and the user has been idle for 29 minutes, the next refresh will succeed and issue a new refresh token — but that token will expire in 1 minute because the SSO session is about to end, regardless of the configured refresh token lifespan. For full details on how these values interact, see Keycloak session timeout configuration.
Refresh token rotation is one of the highest-leverage security controls available in Keycloak. It is a single realm-level toggle that eliminates the silent credential theft scenario for all active sessions. Enable it with refreshTokenMaxReuse: 0, make sure your application code always stores the newest refresh token from every response, and pair it with appropriately short access token lifetimes.
Ready to run Keycloak without managing the infrastructure? See Skycloak’s managed plans.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.