Why a Website Returns 401
A client sends an HTTP request to access a protected resource or API endpoint, and the server replies with status code 401 Unauthorized. The user is either logged out, their session is abruptly interrupted, or an automated integration script fails to authenticate.
401 Unauthorized (Response includes: WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired").Why does Why a Website Returns 401 occur?
Primary Root Cause
The server endpoint is protected by an authentication guard (middleware, gateway, or basic auth challenge). When the request arrived, the server checked for an Authorization header or authentication cookie, found none, or detected that the cryptographic signature, expiration timestamp, or session ID was invalid.
HTTP authentication follows a challenge-response pattern defined in RFC 9110 Section 15.5.2. If a client attempts an unauthenticated request to a protected URL, the server returns 401 with a `WWW-Authenticate` header describing the authentication scheme (e.g. `WWW-Authenticate: Bearer realm="api"`). The client is expected to present valid credentials in subsequent requests.
How to Confirm You Are Facing This Exact Issue
The browser displays a native operating system login popup asking for Username and Password (HTTP Basic Auth).
API returns JSON payload: {"error": "unauthorized", "message": "jwt expired"}.
The user is suddenly redirected back to the login page mid-workflow.
API requests pass locally in Postman or development but consistently fail with 401 in production.
Explicit Diagnostic Reasoning
Follow these diagnostic steps to identify why authentication failed:
"If 401 occurs after 15 to 60 minutes of active usage"
The short-lived JWT access token expired and the frontend failed to execute a silent refresh token rotation.
→ Inspect the "exp" claim using a JWT decoder tool and verify the frontend refresh token interceptor.
"If 401 occurs on all API requests in production behind an Apache or Nginx reverse proxy"
The reverse proxy is stripping the Authorization HTTP header before forwarding requests to the upstream application.
→ Add "proxy_set_header Authorization $http_authorization;" in Nginx or "SetEnvIf Authorization" in Apache.
"If a native browser modal prompts for username/password"
The server returned "WWW-Authenticate: Basic realm=...", triggering the browser’s built-in basic auth dialog.
→ Provide valid basic auth credentials or reconfigure server .htpasswd settings.
"If the server returns 401 with "WWW-Authenticate: Bearer error=invalid_signature""
The JWT was signed with a different secret or private key than the backend validation service expects.
→ Verify that the secret string or JWKS endpoint URL matches between the auth server and API gateway.
Inspects the 401 response headers to check the WWW-Authenticate realm and error parameters.
Long-Term Engineering Solution
Implement automatic token refresh mechanisms in client HTTP interceptors, ensure correct header propagation across proxies, and validate credential expiration before dispatch.
Architectural Pattern
Pair short-lived access tokens (15 minutes) with secure, HttpOnly refresh tokens (7 days) and implement an automatic Axios/Fetch 401 interceptor that refreshes the token seamlessly without logging the user out.
How do you fix this step-by-step?
Attach Authorization Header to Outgoing Requests
Ensure the client attaches the Bearer token with correct syntax (word "Bearer" followed by a single space and the token).
const token = localStorage.getItem('access_token');
const response = await fetch('https://api.example.com/user/profile', {
headers: {
// Note space between 'Bearer' and the token
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});Preserve Authorization Header in Reverse Proxy
Configure Nginx or Apache to forward the Authorization header to upstream application servers.
# Nginx reverse proxy configuration
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# CRITICAL: Forward Authorization header
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}Implement Silent Refresh Interceptor
Set up an Axios or fetch response interceptor that catches 401 errors, requests a new access token, and retries the original request.
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.example.com' });
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Catch 401 and avoid infinite loops
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post('/auth/refresh', {}, { withCredentials: true });
api.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
return api(originalRequest); // Retry original call
} catch (refreshErr) {
window.location.href = '/login'; // Refresh failed, redirect to login
}
}
return Promise.reject(error);
}
);Verify Token Signature and Expiration Locally
Inspect token payload claims to ensure the token has not expired and holds the expected issuer and audience.
# Inspect token claims via Curious-Techie JWT Decoder
# Or decode payload in Node.js:
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64').toString()))" "<YOUR_JWT_HERE>"What are common misconceptions and mistakes when fixing this?
Omitting the "Bearer " prefix and sending only the raw JWT string.
Ursache des Fehlers: Backend frameworks expecting OAuth 2.0 Bearer tokens fail to parse the header and reject it with 401 Malformed Authorization Header.
✓ Korrekte Alternative: Always format the header as `Authorization: Bearer <token>` with a single space.
Returning 401 when the user lacks permission for an admin resource.
Ursache des Fehlers: If the user is already authenticated, returning 401 causes client apps to attempt a re-login loop. The correct status is 403 Forbidden.
✓ Korrekte Alternative: Return 401 only when authentication credentials are absent or invalid. Return 403 when the user is authenticated but not authorized.
Failing to include the WWW-Authenticate header in 401 responses.
Ursache des Fehlers: RFC 9110 explicitly mandates that 401 responses must specify the authentication scheme, causing automated API clients to fail compliant handshakes.
✓ Korrekte Alternative: Always return `WWW-Authenticate: Bearer error="invalid_token"` in 401 API responses.
Can this fix introduce security vulnerabilities?
401 Unauthorized status is the gatekeeper of application privacy, preventing anonymous access to protected data.
Security Risks If Ignored
- •Credential Stuffing: Endpoints that leak detailed 401 error messages ("Username not found" vs "Wrong password") allow account enumeration.
- •Token Leakage: Storing JWT tokens insecurely in LocalStorage exposes them to Cross-Site Scripting (XSS) extraction.
- •Infinite Refresh Loops: Buggy 401 interceptors can trigger denial-of-service hammering against auth servers.
Hardening Best Practices
- ✓Store refresh tokens in secure, HttpOnly, SameSite=Strict cookies.
- ✓Return generic authentication failure messages ("Invalid credentials") to prevent username harvesting.
- ✓Implement rate limiting (e.g. 5 attempts per minute) on endpoints returning 401 to prevent brute-force attacks.
Validating Authorization Header in Express.js Backend Middleware
// BROKEN: Does not check format or return WWW-Authenticate header
app.get('/api/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ error: 'No token' }); // Non-compliant RFC response
}
// Throws unhandled error if token is malformed
const user = jwt.verify(token, 'secret');
res.json({ user });
});// PRODUCTION COMPLIANT AUTHENTICATION GUARD
app.get('/api/protected', (req, res) => {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.setHeader('WWW-Authenticate', 'Bearer realm="api", error="invalid_request"');
return res.status(401).json({ error: 'Missing or malformed Authorization header' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
res.json({ status: 'success', data: req.user });
} catch (err) {
const errorType = err.name === 'TokenExpiredError' ? 'expired_token' : 'invalid_token';
res.setHeader('WWW-Authenticate', `Bearer realm="api", error="${errorType}"`);
return res.status(401).json({ error: 'Token invalid or expired', code: errorType });
}
});Frequently Asked Questions About Why a Website Returns 401
Why does my API return 401 instead of 403?↓
401 indicates that no valid credentials were submitted or the token expired. 403 means valid credentials were provided, but the account lacks permission for that action.
Think of 401 as a locked front door requiring an ID badge. If you have no badge or your badge is expired, you get 401. If you enter the building but try to walk into the CEO’s private office, your badge is rejected with 403 Forbidden.
Can 401 be triggered by CORS preflight issues?↓
Yes. If your backend authentication middleware intercepts HTTP OPTIONS preflight requests and rejects them because they lack a Bearer token, the browser blocks the call with a CORS/401 error.
CORS preflight (OPTIONS) requests never include authorization headers or cookies by browser security design. If backend middleware requires auth on all routes including OPTIONS, preflights will receive 401 and fail immediately.
How should frontend applications handle 401 responses?↓
Attempt a single silent token refresh call. If the refresh succeeds, retry the original request. If the refresh fails, clear auth state and redirect the user to login.
Modern single-page applications use HTTP interceptors to handle 401 errors seamlessly. This ensures users do not lose form input or get violently logged out when an access token reaches its 15-minute expiration limit.