How to fix Error: TokenExpiredError: jwt expired in Node.js
This error occurs when attempting to verify a JSON Web Token (JWT) that has passed its expiration time. The jsonwebtoken library throws a TokenExpiredError when the token's exp claim indicates it is no longer valid.
Quick fix
Fixes TokenExpiredError
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log('Token is valid:', decoded);
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log('Token expired at:', err.expiredAt);
// Trigger token refresh or prompt re-authentication
return { error: 'Token expired', code: 'TOKEN_EXPIRED' };
} else if (err.name === 'JsonWebTokenError') {
console.log('Invalid token:', err.message);
return { error: 'Invalid token', code: 'INVALID_TOKEN' };
}
throw err;
}- Implement proper error handling for expired tokens
- Implement refresh token mechanism
- Configure appropriate token expiration times
- Add clock tolerance for server time differences
const jwt = require('jsonwebtoken');try {const decoded = jwt.verify(token, process.env.JWT_SECRET);console.log('Token is valid:', decoded);} catch (err) {if (err.name === 'TokenExpiredError') {console.log('Token expired at:', err.expiredAt);// Trigger token refresh or prompt re-authenticationreturn { error: 'Token expired', code: 'TOKEN_EXPIRED' };} else if (err.name === 'JsonWebTokenError') {console.log('Invalid token:', err.message);return { error: 'Invalid token', code: 'INVALID_TOKEN' };}throw err;}Error: TokenExpiredError: jwt expired
What this error means
The TokenExpiredError is thrown by the jsonwebtoken library when you attempt to verify a JWT token that has exceeded its expiration time. JWT tokens include an "exp" (expiration) claim that specifies a timestamp after which the token should no longer be accepted. This security mechanism ensures that tokens have a limited lifespan, reducing the risk if a token is compromised. When jwt.verify() is called on an expired token, the library checks the exp claim against the current time and throws this error if the token is no longer valid. The error object includes an expiredAt property that shows exactly when the token expired, which can be useful for debugging and implementing refresh token logic. This is expected behavior and part of JWT's security model - tokens are meant to expire to limit the window of vulnerability if they are stolen or leaked.
Typical symptoms
- API requests fail with 401 Unauthorized after previously working
- User authentication stops working after a period of inactivity
- Error message shows "jwt expired" in logs or API responses
- Token verification throws TokenExpiredError exception
- Application prompts for re-login after token expiration time passes
Common causes
- Token has genuinely expired based on the exp claim value
- Access token lifetime is too short for the application use case
- Client-side code doesn't implement token refresh logic
- Clock skew between token issuer and verifier servers
- User session extends beyond the configured token expiration time
- Token is being reused after expiration instead of refreshing
How to fix "Error: TokenExpiredError: jwt expired"
Catch the TokenExpiredError specifically and handle it appropriately in your application:
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log('Token is valid:', decoded);
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log('Token expired at:', err.expiredAt);
// Trigger token refresh or prompt re-authentication
return { error: 'Token expired', code: 'TOKEN_EXPIRED' };
} else if (err.name === 'JsonWebTokenError') {
console.log('Invalid token:', err.message);
return { error: 'Invalid token', code: 'INVALID_TOKEN' };
}
throw err;
}This allows your application to differentiate between expired tokens and other JWT errors.
Use a dual-token approach with short-lived access tokens and longer-lived refresh tokens:
// Generate access and refresh tokens
const accessToken = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // Short-lived access token
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' } // Longer-lived refresh token
);
// Store refresh token in database
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
// Send both tokens to client
res.json({ accessToken, refreshToken });Create an endpoint to refresh the access token:
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
// Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// Check if refresh token exists in database
const storedToken = await prisma.refreshToken.findFirst({
where: { token: refreshToken, userId: decoded.userId },
});
if (!storedToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid or expired refresh token' });
}
});Set token expiration based on your security requirements and user experience needs:
// For high-security applications
const accessToken = jwt.sign(payload, secret, { expiresIn: '5m' });
// For standard web applications
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// For mobile apps with less frequent API calls
const accessToken = jwt.sign(payload, secret, { expiresIn: '1h' });
// You can also use seconds
const accessToken = jwt.sign(payload, secret, { expiresIn: 900 }); // 15 minutesCommon expiration patterns:
- Access tokens: 5-30 minutes
- Refresh tokens: 1-7 days
- Remember-me tokens: 30-90 days
If you have multiple servers with slight clock differences, use the clockTolerance option:
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
clockTolerance: 10, // Allow 10 seconds of clock skew
});
} catch (err) {
// Handle error
}This helps prevent false TokenExpiredError exceptions due to minor time synchronization issues between servers.
On the client side, automatically refresh tokens before they expire or when receiving a 401 error:
// Axios interceptor example
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// If error is 401 and we haven't retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Attempt to refresh the token
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/auth/refresh', {
refreshToken,
});
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh failed, redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);Advanced notes
Security Considerations: Store refresh tokens securely - preferably in HTTP-only, secure cookies rather than localStorage to prevent XSS attacks. Always use HTTPS in production to prevent token interception. Implement refresh token rotation where each refresh generates both a new access token and a new refresh token, invalidating the old refresh token.
Token Revocation: Maintain a database table of valid refresh tokens so you can revoke them when needed (user logout, password change, security breach). Access tokens cannot be revoked before expiration, which is why they should have short lifespans.
Clock Synchronization: In distributed systems, ensure all servers have synchronized clocks using NTP (Network Time Protocol). Even small time differences can cause tokens to be rejected prematurely or accepted after expiration.
Testing Expired Tokens: During development, you can generate tokens with very short expiration times for testing, or use the ignoreExpiration option (only in non-production environments) to bypass expiration checks:
// NEVER use in production!
const decoded = jwt.verify(token, secret, { ignoreExpiration: true });Alternative Libraries: Consider using more modern token handling libraries like @auth/core or passport-jwt which provide built-in token refresh mechanisms and better security defaults.
Frequently asked questions
- What does "TokenExpiredError" mean?
- The TokenExpiredError is thrown by the jsonwebtoken library when you attempt to verify a JWT token that has exceeded its expiration time. JWT tokens include an "exp" (expiration) claim that specifies a timestamp after which the token should no longer be accepted. This security mechanism ensures that tokens have a limited lifespan, reducing the risk if a token is compromised.
- How do I fix "TokenExpiredError"?
- This error occurs when attempting to verify a JSON Web Token (JWT) that has passed its expiration time. The jsonwebtoken library throws a TokenExpiredError when the token's exp claim indicates it is no longer valid.
- What causes "TokenExpiredError"?
- Token has genuinely expired based on the exp claim value Access token lifetime is too short for the application use case Client-side code doesn't implement token refresh logic
- Is the "TokenExpiredError" error serious?
- It is a medium-severity error โ more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- ๐Node.js Official Documentation
- ๐jsonwebtoken npm package documentation(Official Docs)
- ๐
- ๐TokenExpiredError handling issue(GitHub)