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.
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.
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);
}
);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.
Error: Listener already called (once event already fired)
EventEmitter listener already called with once()
Error: EACCES: permission denied, open '/root/file.txt'
EACCES: permission denied
Error: Invalid encoding specified (stream encoding not supported)
How to fix Invalid encoding error in Node.js readable streams
Error: EINVAL: invalid argument, open
EINVAL: invalid argument, open
TypeError: readableLength must be a positive integer (stream config)
TypeError: readableLength must be a positive integer in Node.js streams