What status code should I use when session token is invalid?
Categories:
Choosing the Right HTTP Status Code for Invalid Session Tokens

Navigate the complexities of HTTP status codes when a session token is invalid, ensuring secure and user-friendly API responses.
When building secure web services, handling authentication and authorization failures gracefully is paramount. A common scenario involves a client sending a request with a session token that is either expired, malformed, or simply not recognized by the server. The HTTP status code returned in such cases is crucial for both client-side error handling and maintaining a robust API. This article explores the most appropriate HTTP status codes for invalid session tokens, discussing the nuances of each and providing best practices.
Understanding the Problem: Invalid Session Tokens
An invalid session token can arise from several situations:
- Expired Token: The token's validity period has passed.
- Malformed Token: The token does not conform to the expected format (e.g., JWT signature invalid, incorrect encoding).
- Revoked Token: The token was explicitly invalidated by the server (e.g., user logged out, password changed).
- Non-existent Token: The token provided does not correspond to any active session on the server.
- Unauthorized Access: The token is valid but does not grant access to the requested resource (this is a different scenario, typically handled by 403 Forbidden).
flowchart TD A[Client Request with Token] --> B{Is Token Present?} B -- No --> C[401 Unauthorized] B -- Yes --> D{Is Token Valid Format?} D -- No --> E[400 Bad Request] D -- Yes --> F{Is Token Expired/Revoked?} F -- Yes --> G[401 Unauthorized] F -- No --> H{Is Token Authorized for Resource?} H -- No --> I[403 Forbidden] H -- Yes --> J[200 OK / Resource Access]
Decision flow for handling session tokens in an API request.
Primary Candidates for Invalid Session Tokens
The two most commonly debated status codes for invalid session tokens are 401 Unauthorized
and 400 Bad Request
. Let's delve into their definitions and appropriate use cases.
401 Unauthorized: The Most Common Choice
The 401 Unauthorized
status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource. This is often accompanied by a WWW-Authenticate
header, though it's not strictly required for token-based authentication where the client already knows the authentication scheme.
Why it fits:
- Authentication Failure: An invalid session token means the client has failed to authenticate itself as a legitimate user.
- Clear Instruction: It signals to the client that re-authentication (e.g., logging in again, refreshing a token) is required.
- Standard Practice: Widely adopted in RESTful APIs for expired, revoked, or non-existent tokens.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="api"
{
"error": "invalid_token",
"message": "Session token is expired or invalid. Please log in again."
}
Example 401 Unauthorized response for an invalid session token.
400 Bad Request: When the Token is Malformed
The 400 Bad Request
status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
Why it fits (sometimes):
- Malformed Syntax: If the token itself is syntactically incorrect (e.g., a JWT with an invalid structure, missing parts, or incorrect encoding), it can be argued that the request is malformed.
- Client Error: The client sent something the server couldn't even parse as a valid token candidate.
Why it might not fit (often):
- Ambiguity:
400
is very generic. It doesn't specifically point to an authentication issue, which401
does. - Token Validity vs. Format: Even a syntactically valid token can be expired or non-existent, which is an authentication failure, not a malformed request.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "malformed_token",
"message": "The provided session token is syntactically incorrect."
}
Example 400 Bad Request response for a malformed session token.
Recommendation and Best Practices
For most scenarios involving an invalid session token (expired, revoked, non-existent), 401 Unauthorized
is the most semantically appropriate and widely understood status code. It clearly communicates an authentication failure.
Reserve 400 Bad Request
for cases where the token itself is so malformed that the server cannot even attempt to validate it against its authentication mechanisms. However, even in these cases, some argue that 401
is still acceptable as the ultimate failure is authentication.
Key Best Practices:
- Consistency: Choose one approach and stick to it across your API.
- Clear Error Messages: Always provide a descriptive error message in the response body, regardless of the status code. This helps clients understand why the request failed.
- Distinguish with Error Codes: Use custom error codes within your JSON response body to differentiate between various types of invalid token issues (e.g.,
token_expired
,token_revoked
,token_malformed
). WWW-Authenticate
Header: For401 Unauthorized
, consider including aWWW-Authenticate
header, especially if you support multiple authentication schemes or want to guide the client on how to re-authenticate.
401 Unauthorized
for any issue that prevents the server from identifying the client as a legitimate, authenticated user. It's more specific than 400 Bad Request
for authentication failures.Example Implementation (Node.js/Express)
Here's a simplified example of how you might handle invalid tokens in a Node.js Express application.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = 'your_secret_key'; // In a real app, use environment variables
// Middleware to verify session token
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) {
return res.status(401).json({ error: 'missing_token', message: 'Authentication token is required.' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'token_expired', message: 'Session token has expired. Please log in again.' });
} else if (err.name === 'JsonWebTokenError') {
// This covers malformed tokens, invalid signatures, etc.
return res.status(401).json({ error: 'invalid_token', message: 'Session token is invalid or malformed.' });
} else {
// Generic error
return res.status(401).json({ error: 'authentication_failed', message: 'Authentication failed.' });
}
}
req.user = user;
next();
});
}
// Protected route
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({ message: `Welcome ${req.user.username}! You have access.` });
});
// Login route to get a token (simplified)
app.post('/login', (req, res) => {
// In a real app, validate credentials against a database
const user = { username: 'testuser' };
const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' });
res.json({ token: token });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Node.js Express middleware for handling session token validation.