Definition
401 Unauthorized and 403 Forbidden are two distinct HTTP status codes that are often confused. Understanding the difference is critical for API security and proper authentication/authorization flows.
401 Unauthorized:
- Meaning: The request requires authentication (user must identify themselves)
- Reason: Missing, invalid, or expired authentication credentials
- Solution: Client must provide valid credentials (login, refresh token)
- Header: Must include
WWW-Authenticateheader specifying auth method
403 Forbidden:
- Meaning: The server understood the request but refuses to authorize it
- Reason: User is authenticated but lacks permission to access the resource
- Solution: Client cannot fix this; requires permission change on server
- Header: No
WWW-Authenticateheader required
Key Distinction:
- 401 β “I don’t know who you are” (identity problem)
- 403 β “I know who you are, but you can’t do that” (permission problem)
Example
401 Unauthorized - Missing Token:
GET /api/admin/users HTTP/1.1
Host: api.example.com
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com"
Content-Type: application/json
{
"error": "Unauthorized",
"message": "Authentication required. Please provide a valid access token."
}
401 Unauthorized - Expired Token:
GET /api/admin/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.EXPIRED_TOKEN
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Token expired"
Content-Type: application/json
{
"error": "Unauthorized",
"message": "Access token has expired. Please refresh your token.",
"expiredAt": "2026-01-09T09:00:00Z"
}
403 Forbidden - Insufficient Permissions (Regular User Accessing Admin):
GET /api/admin/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.VALID_TOKEN
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "Forbidden",
"message": "You do not have permission to access this resource. Admin role required.",
"requiredRole": "admin",
"userRole": "user"
}
403 Forbidden - Account Suspended:
POST /api/posts HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.VALID_BUT_SUSPENDED_USER
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "Forbidden",
"message": "Your account has been suspended. Contact support for assistance.",
"suspendedAt": "2026-01-08T15:00:00Z",
"reason": "Terms of Service violation"
}
Code Example
JavaScript (Fetch API):
const fetchAdminUsers = async () => {
try {
const response = await fetch('https://api.example.com/admin/users', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Accept': 'application/json'
}
});
// Handle 401 Unauthorized
if (response.status === 401) {
const error = await response.json();
console.error('401 Unauthorized:', error.message);
// Check if token expired
if (error.message.includes('expired')) {
console.log('Token expired, attempting refresh...');
// Try to refresh token
const refreshed = await refreshAccessToken();
if (refreshed) {
// Retry request with new token
return fetchAdminUsers();
}
}
// Redirect to login
console.log('Redirecting to login...');
window.location.href = '/login';
return null;
}
// Handle 403 Forbidden
if (response.status === 403) {
const error = await response.json();
console.error('403 Forbidden:', error.message);
// Show user-friendly message
alert('Access Denied: ' + error.message);
// Redirect to dashboard (no point in retrying)
window.location.href = '/dashboard';
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching admin users:', error);
throw error;
}
};
const refreshAccessToken = async () => {
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('https://api.example.com/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
if (response.status === 401) {
// Refresh token also expired
console.error('Refresh token expired, must re-login');
return false;
}
if (response.ok) {
const data = await response.json();
localStorage.setItem('token', data.accessToken);
console.log('Access token refreshed successfully');
return true;
}
return false;
} catch (error) {
console.error('Error refreshing token:', error);
return false;
}
};
Python (requests library):
import requests
def fetch_admin_users():
try:
token = get_token_from_storage()
response = requests.get(
'https://api.example.com/admin/users',
headers={
'Authorization': f'Bearer {token}',
'Accept': 'application/json'
}
)
# Handle 401 Unauthorized
if response.status_code == 401:
error = response.json()
print(f'401 Unauthorized: {error.get("message")}')
# Check if token expired
if 'expired' in error.get('message', '').lower():
print('Token expired, attempting refresh...')
# Try to refresh token
if refresh_access_token():
# Retry request with new token
return fetch_admin_users()
# Redirect to login
print('Authentication required, redirecting to login...')
return None
# Handle 403 Forbidden
if response.status_code == 403:
error = response.json()
print(f'403 Forbidden: {error.get("message")}')
# Show user-friendly message
print(f'Access Denied: {error.get("message")}')
# No point in retrying (permission won't change)
return None
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f'Error fetching admin users: {e}')
raise
def refresh_access_token():
try:
refresh_token = get_refresh_token_from_storage()
response = requests.post(
'https://api.example.com/auth/refresh',
json={'refreshToken': refresh_token}
)
if response.status_code == 401:
# Refresh token also expired
print('Refresh token expired, must re-login')
return False
if response.ok:
data = response.json()
save_token_to_storage(data['accessToken'])
print('Access token refreshed successfully')
return True
return False
except requests.exceptions.RequestException as e:
print(f'Error refreshing token: {e}')
return False
Diagram
flowchart TB
START[API Request] --> HAS_TOKEN{Has Auth Token?}
HAS_TOKEN -->|No| RETURN_401[Return 401
WWW-Authenticate header]
HAS_TOKEN -->|Yes| VALIDATE{Token Valid?}
VALIDATE -->|Invalid/Expired| RETURN_401_EXPIRED[Return 401
Token expired/invalid]
VALIDATE -->|Valid| CHECK_PERM{Has Permission?}
CHECK_PERM -->|No| RETURN_403[Return 403
Insufficient permissions]
CHECK_PERM -->|Yes| RETURN_200[Return 200
Success]
RETURN_401 --> CLIENT_LOGIN[Client: Login/Provide Token]
RETURN_401_EXPIRED --> CLIENT_REFRESHClient: [Refresh Token]
RETURN_403 --> CLIENT_REQUEST[Client: Request Access
or Give Up]
RETURN_200 --> CLIENT_SUCCESS[Client: Process Response]
style RETURN_401 fill:#ff6b6b
style RETURN_401_EXPIRED fill:#ff6b6b
style RETURN_403 fill:#ffa726
style RETURN_200 fill:#66bb6a
Security Notes
SECURITY NOTES
CRITICAL - Always use 401 for authentication failures and 403 for authorization failures. Never swap these codes as it confuses clients and creates security vulnerabilities. Always include WWW-Authenticate header with 401 responses. Never expose internal permission logic in 403 error messages. Log all 401 and 403 responses for security monitoring. Use 403 for rate limiting on authenticated endpoints instead of 401. Never use 404 to hide resources from unauthorized users (security through obscurity). Implement proper token expiration and refresh flows to minimize 401 errors.
Analogy
Think of 401 vs 403 like entering a building:
- 401 Unauthorized β “Show me your ID badge” (guard at entrance)
- 403 Forbidden β “Your badge works, but you can’t access the executive floor” (elevator restriction)
You need proper identification (401) before they can even check if you have access (403).
Best Practices
- Use 401 for Authentication - Missing, invalid, or expired credentials
- Use 403 for Authorization - Valid credentials but insufficient permissions
- Include WWW-Authenticate - Always add this header to 401 responses
- Provide Clear Messages - Explain why access was denied
- Never Swap Codes - Don’t use 401 when you mean 403 or vice versa
- Log Security Events - Track 401/403 responses for intrusion detection
- Implement Token Refresh - Use refresh tokens to minimize 401 errors
- Don’t Hide Resources - Use proper 403/401, not 404 for security
Common Mistakes
- Using 401 for Authorization - Returning 401 when user lacks permissions (should be 403)
- Using 403 for Authentication - Returning 403 when token is missing/expired (should be 401)
- No WWW-Authenticate Header - Not including required header with 401 responses
- 404 for Security - Hiding resources with 404 instead of proper 401/403
- Exposing Permission Logic - Including too much detail in 403 error messages
- No Retry for 401 - Not implementing token refresh flows in clients
- Retrying 403 - Clients retrying 403 errors (which won’t change without server-side permission update)
Standards & RFCs
Standards & RFCs