How to Fix 'JWT Expired' Errors: Addressing Clock Skew and Refresh Token Gaps
Learn to diagnose and fix 'JWT expired' errors by correcting system clock discrepancies, implementing robust refresh token mechanisms, and understanding token
When a JSON Web Token (JWT) expires, it means its exp (expiration) claim indicates the token is no longer valid, leading to authentication failures. To fix this, you typically need to obtain a new, valid token, either by refreshing an existing session using a refresh token or by re-authenticating. However, the root cause can be more complex than a simple timeout, often involving system clock issues or missing token management strategies.
Resolving ‘JWT Expired’ Due to System Clock Discrepancies
One of the most perplexing causes of a “JWT expired” error is when the token appears to be valid but is rejected due to time synchronization issues. This occurs when there’s a “clock skew” between the system verifying the token (e.g., a server) and the system that issued it, or between a client device and the server,. A token issued with an expiration time might be considered expired prematurely if the verifying system’s clock is ahead, or if the client’s clock is behind, making a valid token seem invalid,.
For End-Users (Client-Side Fix):
If you encounter a “JWT expired” error in an application, especially after your device has been offline or its time settings were manually adjusted, ensure your device’s clock is correctly synchronized. For Windows users, this involves navigating to Settings > Time and language > Date and time and enabling Set time automatically and Set time zone automatically. If the time zone is incorrect, disable automatic setting, pick the correct zone, and then select Sync now. A restart of the PC is recommended afterward.
For Developers (Server-Side Fix):
To mitigate clock skew issues, implement a clockTolerance (also known as leeway) when verifying JWTs. This allows for a small window of time (e.g., 60 seconds) during which a token is still considered valid even if its exp claim is slightly in the past relative to the verifying server’s clock. Additionally, ensure all systems involved (issuing server, verifying server) use Coordinated Universal Time (UTC) for timestamps to prevent timezone-related discrepancies,.
Worked Example: Implementing Clock Tolerance in Token Verification
Consider a scenario where a token expires at 1678886400 (Unix timestamp for March 15, 2023, 12:00:00 PM UTC). If your server’s clock is 30 seconds ahead, it would perceive the current time as 1678886430 when it’s actually 1678886400. Without tolerance, the token would be rejected. By adding a clockTolerance of 60 seconds, the server will accept tokens whose exp claim is up to 60 seconds in the past or future relative to its current time.
Here’s how you might implement this in a Python JWT library, using a leeway parameter:
import jwt
import time
from datetime import datetime
def verify_token_with_tolerance(token_string, secret_key):
try:
# Example token with exp: 1678886400 (March 15, 2023, 12:00:00 PM UTC)
# For demonstration, assume current time is 1678886430 (30 seconds past expiration)
# leeway=60 allows for 60 seconds of clock skew
payload = jwt.decode(
token_string,
secret_key,
algorithms=["HS256"],
leeway=60 # 60 seconds tolerance
)
print(f"Token is valid. Payload: {payload}")
return {"valid": True, "payload": payload}
except jwt.ExpiredSignatureError:
print("Token expired, even with tolerance.")
return {"valid": False, "error": "Token expired"}
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
return {"valid": False, "error": str(e)}
# Example usage (replace with your actual token and secret)
# For this example, let's assume a token was generated with exp=1678886400
# and the current system time is slightly past that.
example_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJhYmMxMjMiLCJleHAiOjE2Nzg4ODY0MDB9.signature_placeholder"
example_secret = "your-super-secret-key"
# To simulate, we'd need to mock time.time() or generate a token with a past exp
# and then run this function. If current time is 1678886430, with leeway=60,
# the token (exp=1678886400) would be considered valid.
# If current time is 1678886461, it would be considered expired.
# In a real scenario, jwt.decode would use the actual current time.
# Let's assume the current time is 1678886430 (30 seconds after exp)
# With leeway=60, 1678886400 + 60 = 1678886460. Since 1678886430 < 1678886460, it's valid.
# If current time was 1678886461, then 1678886461 > 1678886460, so it would be expired.
Addressing Persistent Expiration with a Robust Refresh Token Mechanism
A common architectural flaw leading to frequent “JWT expired” errors is the absence or improper implementation of a token refresh mechanism. JWTs are designed to be short-lived for security reasons, typically expiring within 15 minutes,. Without a way to automatically obtain a new access token, users would be forced to log in repeatedly.
The standard solution is to use a refresh token alongside the access token. When a user logs in, the authentication server issues both a short-lived access token (e.g., 15 minutes) and a longer-lived refresh token (e.g., 7 days).
How the Refresh Token Flow Works:
- Initial Login: The client authenticates with credentials and receives an access token and a refresh token.
- API Requests: The client uses the access token for subsequent requests to protected API endpoints.
- Access Token Expiration: When the access token expires, the client’s API requests will fail with an authentication error (e.g., 401 Unauthorized).
- Token Refresh: Instead of prompting the user to log in again, the client sends the refresh token to a dedicated refresh endpoint on the authentication server.
- New Access Token: The authentication server validates the refresh token and, if valid, issues a new access token (and often a new refresh token as well).
- Retry Request: The client then retries the original failed API request with the newly acquired access token.
Worked Example: Client-Side Token Refresh Logic
This example outlines the client-side logic for handling an expired access token using a refresh token. It assumes you have functions to store/retrieve tokens and make API calls.
// Fictional client-side code demonstrating refresh flow
let currentAccessToken = localStorage.getItem('accessToken');
let currentRefreshToken = localStorage.getItem('refreshToken');
async function makeAuthenticatedRequest(url, options = {}) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${currentAccessToken}`
};
let response = await fetch(url, options);
// If access token expired (e.g., server returns 401)
if (response.status === 401) {
console.log('Access token expired. Attempting to refresh...');
const refreshSuccessful = await refreshAccessToken();
if (refreshSuccessful) {
// Retry the original request with the new access token
options.headers['Authorization'] = `Bearer ${currentAccessToken}`;
response = await fetch(url, options);
} else {
// Refresh failed, force user to re-login
console.error('Refresh token invalid or expired. User must re-login.');
// Redirect to login page or show re-login prompt
return null; // Or throw an error
}
}
return response;
}
async function refreshAccessToken() {
try {
const refreshResponse = await fetch('/auth/refresh-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: currentRefreshToken })
});
if (refreshResponse.ok) {
const data = await refreshResponse.json();
currentAccessToken = data.newAccessToken; // Update global access token
currentRefreshToken = data.newRefreshToken; // Update global refresh token (optional, but good practice)
localStorage.setItem('accessToken', currentAccessToken);
localStorage.setItem('refreshToken', currentRefreshToken);
console.log('Tokens refreshed successfully.');
return true;
} else {
console.error('Failed to refresh token:', refreshResponse.statusText);
return false;
}
} catch (error) {
console.error('Error during token refresh:', error);
return false;
}
}
// Example usage:
// makeAuthenticatedRequest('/api/user-data').then(response => {
// if (response && response.ok) {
// response.json().then(data => console.log(data));
// }
// });
Before making any API calls, it’s also a good practice to proactively check if the access token is about to expire on the client side. You can decode the JWT to inspect its exp claim. The exp value is a Unix timestamp (seconds since January 1, 1970). You can use a tool like the free JWT Decoder to quickly inspect the exp claim of any token.
Understanding the Simple Case: Genuine Expiration and Re-authentication
Once you’ve ruled out clock synchronization issues and confirmed a proper refresh token mechanism is in place (or implemented one), the simplest reason for a “JWT expired” error is that the access token has genuinely reached its designated expiration time. Access tokens are intentionally short-lived, typically lasting around 15 minutes,.
In scenarios where a refresh token isn’t available (e.g., a public client without persistent sessions) or has also expired, the only fix is for the user to re-authenticate. This means logging in again with their username and password to establish a new secure session and obtain a fresh set of tokens.
For mobile or desktop applications, this often translates to simple user actions:
- Fully close and reopen the application: This forces the app to re-initialize its session, potentially triggering a new login flow or token refresh.
- Uninstall and reinstall the application: This clears all temporary app data, including potentially stale tokens, and ensures a clean slate for re-authentication.
These user-level fixes are effective because they essentially reset the authentication state, allowing the application to request new tokens from the server. From a developer’s perspective, this indicates that the token lifecycle has completed, and a new one needs to be initiated.
A Test Set of Inputs to Check Yourself Against
To ensure you can effectively diagnose and fix JWT expiration issues, consider these scenarios:
- Scenario: User reports “JWT expired” immediately after logging in, but only on their laptop, not their phone.
- Diagnosis: This strongly suggests a client-side clock skew issue on the laptop. The server issued a valid token, but the laptop’s time is ahead, making the token appear expired prematurely.
- Fix: Advise the user to synchronize their laptop’s system clock with an internet time server. Developers should ensure server-side token verification includes
clockTolerance.
- Scenario: All users experience “JWT expired” errors precisely 15 minutes after logging in, requiring them to log in again.
- Diagnosis: The access token is genuinely expiring after its short lifespan (e.g., 15 minutes), and there’s either no refresh token mechanism, or it’s failing silently.
- Fix: Implement a robust refresh token flow where the client automatically uses a long-lived refresh token to obtain a new access token when the current one expires.
- Scenario: A backend service using a JWT for inter-service communication starts failing with “JWT expired” errors after a server restart, even though the token was generated recently.
- Diagnosis: This could indicate a clock skew between the service that generated the token and the service attempting to use/verify it after the restart. The restart might have reset time synchronization or exposed a pre-existing skew.
- Fix: Verify that all servers are synchronized to a reliable NTP (Network Time Protocol) server and are using UTC. Add
clockToleranceto the token verification logic in the consuming service.
- Scenario: A user leaves an application open in the background for several hours, then returns to find a “JWT expired” error, but closing and reopening the app resolves it.
- Diagnosis: The access token likely expired during the long period of inactivity. Reopening the app triggered a new login or refresh process, obtaining a fresh token.
- Fix: This is often expected behavior if the refresh token also has a limited lifespan or if the application doesn’t actively manage background refresh. Ensure the refresh token’s expiry is appropriate for the desired user session length (e.g., 7 days). If the refresh token itself has expired, re-authentication is the correct path.