JWT Token Expired:
How to Fix It
Getting a 401 Unauthorized? Your JWT has probably expired. Here's how to diagnose it, fix it, and prevent it happening again.
A 401 Unauthorized error is one of the most frustrating things to debug in a JWT-authenticated API. The request looks right. The endpoint hasn't changed. But suddenly, everything returns 401. Nine times out of ten, the token has expired.
This guide walks through exactly how to confirm token expiry is the cause, how to fix it in your frontend and backend, and how to build a refresh token flow that prevents the problem from surfacing to users.
Step 1: Confirm the Token is Actually Expired
Don't assume — decode the token and check the exp claim directly. The exp field is a Unix timestamp (seconds since epoch). If it's less than the current time, the token is expired.
The fastest way without writing code: paste the token into ResourceCentral's JWT Decoder. It decodes entirely in your browser, shows the exp as a human-readable timestamp, and flags it red if expired. Your token never leaves your machine.
Or do it in the browser console:
// Decode JWT payload in browser console
const token = 'eyJhbGci...'; // your token
const payload = JSON.parse(atob(token.split('.')[1]));
const expired = payload.exp < Math.floor(Date.now() / 1000);
console.log('Expires:', new Date(payload.exp * 1000).toLocaleString());
console.log('Expired:', expired);
Or in Python:
import base64, json, time
token = "eyJhbGci..."
payload_b64 = token.split('.')[1]
# Pad base64 if needed
payload_b64 += '=' * (4 - len(payload_b64) % 4)
payload = json.loads(base64.b64decode(payload_b64))
print("Expires:", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(payload['exp'])))
print("Expired:", payload['exp'] < time.time())
Common Causes of Premature Expiry
The Fix: Implement a Refresh Token Flow
The proper solution to token expiry is a refresh token flow. Instead of forcing users to re-login when an access token expires, the client silently obtains a new access token using a longer-lived refresh token.
Here's the pattern:
POST /auth/refresh. The server reads the HttpOnly refresh token cookie, validates it, and issues a new access token.Refresh Token Flow: JavaScript Implementation
// Axios interceptor — silently refresh on 401
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
error ? prom.reject(error) : prom.resolve(token);
});
failedQueue = [];
};
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue requests while refresh is in progress
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return axios(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// Refresh token is in HttpOnly cookie — no JS access needed
const { data } = await axios.post('/auth/refresh');
const newToken = data.access_token;
axios.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
processQueue(null, newToken);
originalRequest.headers['Authorization'] = 'Bearer ' + newToken;
return axios(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// Refresh token also expired — redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
Choosing the Right Token Lifetime
| Application Type | Access Token | Refresh Token | Notes |
|---|---|---|---|
| Banking / Healthcare | 5–15 min | 24 hours | Short windows, prompt re-auth on sensitive actions |
| Standard SaaS | 15–60 min | 7–30 days | Good balance of security and UX |
| Mobile app | 1 hour | 90 days | Longer refresh needed for intermittent connectivity |
| Machine-to-machine API | 1 hour | N/A (Client Credentials) | No refresh needed — services request new tokens directly |
Debugging Checklist for 401 JWT Errors
- Decode the token — confirm
expis the issue, not a wrong secret or malformed token - Check clock skew — compare server time (
datecommand) to client time; >60 seconds difference will cause issues - Verify the Authorization header — must be
Bearer <token>with a capital B and a space, no extra quotes - Check the
issandaudclaims — if your validation library checks these, a mismatch returns 401 not 403 - Confirm the signing key matches — HS256 uses a shared secret; RS256 uses a public/private keypair. A mismatch produces a signature validation failure, also a 401
- Look for double-encoding — some proxies base64-encode the Authorization header a second time. Check what actually reaches your API server
Decode Your Token — Free & Private
Check the exp claim, algorithm, and all claims in seconds. Token never leaves your browser.
Open JWT Decoder — Free →FAQ
Why does my JWT keep expiring? +
Because the exp claim is in the past. Common causes: the token lifetime is too short, there's clock skew between client and server, or there's no refresh token flow to silently renew tokens. Check the decoded exp timestamp first to confirm expiry is the actual issue.
Is it safe to extend the token lifetime to avoid this? +
Not as a long-term fix. Long-lived tokens increase the window of exploitation if stolen. The correct solution is a refresh token flow — keep access tokens short-lived (15–60 min) and use a refresh token to issue new ones automatically.
Can I invalidate a JWT before it expires? +
Not without a server-side blocklist. JWTs are stateless by design — once issued, they're valid until expiry unless your validation checks against a revocation list. If you need immediate revocation (e.g. on logout or account compromise), maintain a blocklist of revoked jti claims, or switch to opaque tokens with a session store.
Where should I store the refresh token? +
In an HttpOnly, Secure, SameSite=Strict cookie. This prevents JavaScript access (protecting against XSS) while still being sent automatically on requests to your refresh endpoint. Never store refresh tokens in localStorage.