Keycloak Token Expired: Understanding and Fixing Token Issues
Last updated: March 2026
“Token expired” is one of the most common errors developers encounter when working with Keycloak. The error itself is simple, but the token lifecycle in Keycloak involves multiple interacting timeout values that can make troubleshooting confusing. This guide breaks down every timeout that affects token validity, explains how they interact, and provides concrete fixes for the most common scenarios.
How Keycloak Tokens Work
Keycloak issues three types of tokens during an OpenID Connect authentication flow:
- Access Token: A short-lived JWT that the client presents to resource servers. It contains user claims, roles, and scopes. Default lifetime: 5 minutes.
- Refresh Token: A longer-lived opaque token used to obtain new access tokens without requiring the user to re-authenticate. Default lifetime: 30 minutes.
- ID Token: A JWT containing identity claims about the authenticated user. Used by the client application, not sent to APIs.
You can decode and inspect any of these tokens using the JWT Token Analyzer to see their expiration claims (exp, iat), issued realm, and embedded roles.
The typical token flow looks like this:
- User authenticates with Keycloak and receives all three tokens.
- The application uses the access token for API calls.
- When the access token expires, the application uses the refresh token to get a new access token.
- When the refresh token expires, the user must re-authenticate.
The Five Timeout Settings That Matter
Keycloak has several timeout values that interact to determine how long a user stays authenticated. Understanding each one is essential. The official Keycloak session documentation covers these in detail.
1. Access Token Lifespan
Location: Realm Settings > Tokens > Access Token Lifespan
This controls how long the access token JWT is valid. The exp claim in the token is set to iat + access_token_lifespan.
Default: 5 minutes
Access Token Lifespan = 300 seconds (5 minutes)
A short lifespan limits the damage window if a token is compromised. The trade-off is more frequent token refresh requests.
2. SSO Session Idle Timeout
Location: Realm Settings > Sessions > SSO Session Idle
This is the maximum time a user’s SSO session can remain idle (no token refresh or new authentication) before it expires. When this timeout is reached, all refresh tokens associated with the session become invalid.
Default: 30 minutes
3. SSO Session Max Lifespan
Location: Realm Settings > Sessions > SSO Session Max
The absolute maximum duration of an SSO session, regardless of activity. Even if the user is actively refreshing tokens, the session will expire after this period.
Default: 10 hours
4. Client Session Idle Timeout
Location: Realm Settings > Tokens > Client Session Idle
This is the idle timeout for a specific client’s session within the broader SSO session. It can be shorter than the SSO Session Idle but never longer.
Default: Inherits from SSO Session Idle
5. Client Session Max Lifespan
Location: Realm Settings > Tokens > Client Session Max
The maximum lifespan for a specific client’s session. Like the client idle timeout, it can be shorter than the SSO Session Max but never longer.
Default: Inherits from SSO Session Max
For a comprehensive walkthrough of session timeout best practices, see the companion post on Keycloak session timeout configuration.
How Timeouts Interact
The effective lifetime of a refresh token is determined by the minimum of these values:
Refresh Token Lifetime = min(
SSO Session Idle,
SSO Session Max - elapsed time,
Client Session Idle,
Client Session Max - elapsed time
)
This means that even if your SSO Session Idle is 30 minutes, a Client Session Idle of 10 minutes will cause the refresh token to expire after 10 minutes of inactivity.
Here is a practical example:
| Setting | Value |
|---|---|
| Access Token Lifespan | 5 minutes |
| SSO Session Idle | 30 minutes |
| SSO Session Max | 10 hours |
| Client Session Idle | 15 minutes |
| Client Session Max | 2 hours |
In this configuration:
- The access token is valid for 5 minutes.
- If the user is idle for 15 minutes, the refresh token expires (Client Session Idle).
- Even with continuous activity, the client session ends after 2 hours.
- The SSO session itself can last up to 10 hours if the user is active.
Common Token Expiration Scenarios and Fixes
Scenario 1: Access Token Expired During API Call
Symptom: API returns 401 Unauthorized with Token is not active or Token expired.
Fix: Implement token refresh logic in your application. The access token is intentionally short-lived. Your application should refresh it proactively.
// Token refresh middleware for Axios
import axios from 'axios';
let isRefreshing = false;
let refreshSubscribers = [];
function onRefreshed(newToken) {
refreshSubscribers.forEach((callback) => callback(newToken));
refreshSubscribers = [];
}
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve) => {
refreshSubscribers.push((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(axios(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await refreshAccessToken();
isRefreshing = false;
onRefreshed(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);
} catch (refreshError) {
isRefreshing = false;
// Redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
async function refreshAccessToken() {
const response = await axios.post(
'https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token',
new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'my-client',
refresh_token: getStoredRefreshToken(),
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
storeTokens(response.data);
return response.data.access_token;
}
Scenario 2: Refresh Token Expired
Symptom: Token refresh request returns 400 Bad Request with Session not active or Token is not active.
Fix: This means the SSO or client session has expired. The user must re-authenticate. Check your idle and max timeout values and adjust them based on your use case.
To increase the refresh token lifetime:
- Go to Realm Settings > Sessions.
- Increase SSO Session Idle (e.g., from 30 minutes to 8 hours for a SaaS application).
- Verify that Client Session Idle is not overriding this with a lower value.
Scenario 3: Clock Skew Between Services
Symptom: Tokens appear to expire before they should, or freshly issued tokens are rejected as expired.
Fix: Clock skew occurs when the Keycloak server and the resource server have different system times. Even a 30-second drift can cause problems with a 5-minute access token.
# Check time on your Keycloak server
date -u
# Check time on your API server
date -u
# If using NTP, force sync
sudo ntpdate -u pool.ntp.org
Most JWT validation libraries allow configuring a clock skew tolerance:
// Java with jose4j
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setAllowedClockSkewInSeconds(30)
.setRequireExpirationTime()
.setExpectedIssuer("https://keycloak.example.com/realms/my-realm")
.setVerificationKey(publicKey)
.build();
# Python with PyJWT
import jwt
decoded = jwt.decode(
token,
key=public_key,
algorithms=["RS256"],
leeway=30, # 30 seconds clock skew tolerance
audience="my-client",
)
Scenario 4: Offline Tokens Not Working
Symptom: Offline tokens expire unexpectedly.
Fix: Offline tokens are special refresh tokens that survive server restarts and session cleanup. They have their own timeout settings:
- Go to Realm Settings > Tokens.
- Set Offline Session Idle (default: 30 days).
- Optionally enable Offline Session Max Limited and set a maximum lifespan.
The client must request the offline_access scope to receive an offline token:
curl -X POST "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token"
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=password"
-d "client_id=my-client"
-d "username=user"
-d "password=pass"
-d "scope=openid offline_access"
Token Refresh Strategies by Language
Python
import requests
import time
class KeycloakTokenManager:
def __init__(self, server_url, realm, client_id, client_secret=None):
self.token_url = f"{server_url}/realms/{realm}/protocol/openid-connect/token"
self.client_id = client_id
self.client_secret = client_secret
self.tokens = None
self.expires_at = 0
def get_access_token(self):
if self.tokens and time.time() < self.expires_at - 30:
return self.tokens["access_token"]
if self.tokens and "refresh_token" in self.tokens:
return self._refresh()
raise Exception("No valid token available. Re-authenticate.")
def _refresh(self):
data = {
"grant_type": "refresh_token",
"client_id": self.client_id,
"refresh_token": self.tokens["refresh_token"],
}
if self.client_secret:
data["client_secret"] = self.client_secret
response = requests.post(self.token_url, data=data)
if response.status_code != 200:
raise Exception(f"Token refresh failed: {response.json()}")
self.tokens = response.json()
self.expires_at = time.time() + self.tokens["expires_in"]
return self.tokens["access_token"]
Go
package auth
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type TokenManager struct {
tokenURL string
clientID string
clientSecret string
tokens *TokenResponse
expiresAt time.Time
mu sync.Mutex
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
func (tm *TokenManager) GetAccessToken() (string, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.tokens != nil && time.Now().Before(tm.expiresAt.Add(-30*time.Second)) {
return tm.tokens.AccessToken, nil
}
if tm.tokens != nil && tm.tokens.RefreshToken != "" {
return tm.refresh()
}
return "", fmt.Errorf("no valid token, re-authenticate")
}
func (tm *TokenManager) refresh() (string, error) {
data := url.Values{
"grant_type": {"refresh_token"},
"client_id": {tm.clientID},
"client_secret": {tm.clientSecret},
"refresh_token": {tm.tokens.RefreshToken},
}
resp, err := http.Post(tm.tokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return "", err
}
defer resp.Body.Close()
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", err
}
tm.tokens = &tokenResp
tm.expiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return tokenResp.AccessToken, nil
}
Recommended Timeout Values by Use Case
| Use Case | Access Token | SSO Session Idle | SSO Session Max | Notes |
|---|---|---|---|---|
| Banking / Financial | 2 min | 5 min | 30 min | Strict for compliance |
| SaaS Application | 5 min | 30 min | 12 hours | Balance UX and security |
| E-commerce | 5 min | 1 hour | 24 hours | Longer to avoid cart abandonment |
| Internal Tools | 10 min | 4 hours | 12 hours | Convenience for employees |
| Mobile App | 5 min | 30 days (offline) | 90 days | Use offline tokens |
For managed Keycloak with pre-configured session policies, see the Session Management feature page. You can also review how audit logs help track session events across your realm.
Debugging Token Expiry Issues
When you are unsure why a token was rejected, follow this process:
-
Decode the token: Use the JWT Token Analyzer to inspect the
exp,iat, andauth_timeclaims. -
Check the Keycloak event log: Go to Events > Login Events and look for
REFRESH_TOKENandREFRESH_TOKEN_ERRORevents. The error details will tell you exactly which timeout was exceeded. -
Compare timestamps: Check whether the token’s
expclaim matches what you expect based on your realm settings. -
Verify realm settings via API:
# Get realm configuration (requires admin access)
curl -s "https://keycloak.example.com/admin/realms/my-realm"
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{
accessTokenLifespan,
ssoSessionIdleTimeout,
ssoSessionMaxLifespan,
clientSessionIdleTimeout,
clientSessionMaxLifespan,
offlineSessionIdleTimeout,
offlineSessionMaxLifespan
}'
This outputs all relevant timeout values in one view, making it easy to identify misconfigurations.
For more on Keycloak token validation patterns, see our post on Keycloak token validation for APIs and the guide to JWT token lifecycle management.
Client-Level Token Overrides
Individual clients can override realm-level token settings. This is useful when different applications need different token lifetimes.
To configure per-client overrides:
- Go to Clients > your-client > Advanced Settings.
- Set Access Token Lifespan to override the realm default.
- Set Client Session Idle and Client Session Max for client-specific session limits.
This is particularly useful in multi-tenant setups where different tenants have different security requirements. For a deeper dive into multi-tenancy, see our post on multitenancy in Keycloak using the organizations feature.
Preventing Token Issues in Production
Follow these guidelines to minimize token-related issues:
-
Always implement token refresh logic. Never assume the access token is valid. Check the
expclaim or catch 401 responses and refresh proactively. -
Monitor token refresh failures. Log refresh failures and alert on spikes, which could indicate session configuration problems or Keycloak issues.
-
Use appropriate token lifetimes. A 5-minute access token with a 30-minute refresh token is a reasonable default for most web applications.
-
Sync clocks with NTP. Ensure all servers (Keycloak, API servers, clients) use NTP for time synchronization.
-
Test timeout behavior. During development, temporarily set short timeout values (e.g., 1-minute access tokens) to verify your refresh logic works correctly.
Wrapping Up
Token expiration in Keycloak is governed by a hierarchy of timeout settings at the realm and client level. Most “token expired” issues come down to either missing refresh logic in the application or a mismatch between session timeout values and user expectations.
Start by inspecting the token with the JWT Token Analyzer, check the Keycloak event log, and verify your realm timeout configuration. With the right settings and proper refresh logic, token expiration becomes invisible to your users.
If you want to avoid managing these configurations yourself, Skycloak offers managed Keycloak hosting with expert-configured session policies, built-in monitoring, and 24/7 support — letting you focus on building your application instead of debugging token issues.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.