The Exact Error
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"
{
"error": "Unauthorized",
"message": "No authentication token provided"
}
HTTP/1.1 403 Forbidden
{
"error": "Forbidden",
"message": "You do not have permission to access this resource"
}
Quick summary: 401 = "who are you? send credentials." 403 = "I know who you are, but you can't do this."
Why This Matters
Using the wrong status code causes client-side bugs:
- A 401 response should include a
WWW-Authenticateheader so the client knows how to authenticate - An HTTP client or browser that receives 401 may automatically prompt for credentials or retry with stored credentials
- A 403 signals that retrying with different credentials won't help ā the user needs elevated permissions
The Key Distinction
| Scenario | Correct code | Reason |
|---|---|---|
| No token in request | 401 | Not authenticated |
| Expired JWT token | 401 | Authentication failed |
| Invalid API key | 401 | Authentication failed |
| Token valid but role insufficient | 403 | Authenticated, not authorized |
| Resource belongs to another user | 403 | Authenticated, not authorized |
| IP not whitelisted | 403 | Access denied regardless of auth |
| Resource does not exist (security) | 404 | Hide existence from unauthorized user |
Step-by-Step Diagnosis
Step 1 ā Check if you're sending credentials at all
const response = await fetch('/api/admin', {
headers: {
Authorization: `Bearer ${token}`, // Is this set?
},
});
if (response.status === 401) {
// No valid credentials ā redirect to login
router.push('/login');
}
Step 2 ā Check token validity
function isTokenExpired(token) {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp * 1000 < Date.now();
}
if (isTokenExpired(token)) {
// Refresh the token or redirect to login
}
Step 3 ā Check user permissions
if (response.status === 403) {
// User is logged in but lacks permission
// Show "Access Denied" UI, not login page
showAccessDenied();
}
Solutions
Solution 1 ā Return the right code on the server (Express)
function requireAuth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
function requireRole(role) {
return (req, res, next) => {
if (!req.user.roles.includes(role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Solution 2 ā Handle both on the client
async function apiFetch(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${getToken()}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (response.status === 401) {
clearToken();
window.location.href = '/login';
return;
}
if (response.status === 403) {
throw new Error('Access denied ā insufficient permissions');
}
return response.json();
}
Real-World Examples
AWS API Gateway:
- Returns 401 when the JWT is missing or invalid
- Returns 403 when the JWT is valid but the IAM policy denies the action
GitHub API:
- Returns 401 when no token or bad token
- Returns 403 when rate limited or accessing a private resource without the correct scope
Quick Reference ā Auth Error Status Codes
| Code | Name | When to use | Client action |
|---|---|---|---|
| 401 | Unauthorized | No or invalid credentials | Prompt login / refresh token |
| 403 | Forbidden | Valid credentials, no permission | Show access denied UI |
| 404 | Not Found | Hide sensitive resource existence | Treat as not found |
| 407 | Proxy Auth Required | Proxy needs credentials | Authenticate with proxy |
Prevent This Error in the Future
1. Use 401 only when authentication is the issue ā missing, expired, or invalid credentials.
2. Use 403 when the identity is known but access is denied ā wrong role, wrong scope, resource belongs to someone else.
3. Include WWW-Authenticate in 401 responses ā it tells the client which auth scheme to use.
Use ToolNinja to Debug Faster
The HTTP Status Codes reference gives you the full definition, typical causes, and fix strategies for every HTTP status code.
š§ HTTP Status Codes ā toolninja.io/tools/http-status-codes