401 Unauthorized
What is HTTP 401 Unauthorized?
When you want to see something private on a website - like your messages or acco...
Explain Like I’m 3
You need a special key to open this door, but you don’t have the key! The door says ‘Show me your key!’ but you can’t because you don’t have one. You need to get the key first!
Example: You try to open a treasure chest, but it’s locked. It says ‘Password, please!’ You don’t know the password, so you can’t open it. You need to know the secret password!
Explain Like I’m 5
When you want to see something private on a website - like your messages or account settings - you need to prove who you are first by logging in. If you haven’t logged in yet, or if your password is wrong, or if you were logged in but your ‘session’ expired, the website responds with ‘401 Unauthorized.’ This means ‘I don’t know who you are, so I can’t show you this private information!’ It’s like trying to get into a members-only club without your membership card - the bouncer won’t let you in until you prove you’re a member!
Example: You try to check your messages on a website, but you forgot to log in. The website responds ‘401 Unauthorized - Please log in first!’ You enter your username and password, and then it lets you see your messages.
Jr. Developer
HTTP 401 Unauthorized indicates the request requires authentication and either no credentials were provided, or the provided credentials are invalid. Per RFC 9110, the server MUST send a WWW-Authenticate header indicating the authentication scheme required (Basic, Bearer, Digest, etc.). Common causes: missing Authorization header, expired JWT token, incorrect username/password, invalid API key. Key distinction from 403: 401 means ‘who are you?’ (authentication failed), 403 means ‘I know who you are but you can’t access this’ (authorization failed). After receiving 401, clients should prompt for credentials or refresh tokens. Never automatically retry 401 without updating authentication. For APIs using JWT: return 401 when token is missing, expired, or has invalid signature. Always use HTTPS for endpoints requiring authentication - sending credentials over HTTP is insecure.
Example: User requests GET /api/profile without Authorization header. Server responds 401 with WWW-Authenticate: Bearer realm=‘api’. Client prompts user to log in, gets JWT token, retries with Authorization: Bearer <token>. Request succeeds with 200 OK.
Code Example
// Express.js implementing 401 Unauthorizedconst express = require('express');const jwt = require('jsonwebtoken');const app = express();
const SECRET_KEY = process.env.JWT_SECRET;
// Authentication middlewarefunction requireAuth(req, res, next) { const authHeader = req.headers.authorization;
// No Authorization header if (!authHeader) { return res.status(401) .set('WWW-Authenticate', 'Bearer realm="api"') .json({ error: 'Unauthorized', message: 'Authentication required' }); }
// Extract token const [scheme, token] = authHeader.split(' ');
if (scheme !== 'Bearer' || !token) { return res.status(401) .set('WWW-Authenticate', 'Bearer realm="api"') .json({ error: 'Unauthorized', message: 'Invalid authorization header format' }); }
// Verify token try { const decoded = jwt.verify(token, SECRET_KEY); req.user = decoded; next(); } catch (err) { return res.status(401) .set('WWW-Authenticate', 'Bearer realm="api", error="invalid_token"') .json({ error: 'Unauthorized', message: err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token' }); }}
// Protected endpointapp.get('/api/profile', requireAuth, (req, res) => { res.json({ userId: req.user.id, name: req.user.name });});
// Login endpointapp.post('/api/login', (req, res) => { const { username, password } = req.body;
// Validate credentials (simplified) if (username === 'user' && password === 'pass') { const token = jwt.sign( { id: 1, name: 'User' }, SECRET_KEY, { expiresIn: '1h' } );
return res.json({ token }); }
// Invalid credentials - return 401 res.status(401).json({ error: 'Unauthorized', message: 'Invalid username or password' });});Crash Course
401 Unauthorized, defined in RFC 9110 Section 15.5.2 (obsoleting RFC 7235), indicates the request has not been applied because it lacks valid authentication credentials for the target resource. Per RFC 9110, the server generating a 401 response MUST send a WWW-Authenticate header field containing at least one challenge applicable to the requested resource. The WWW-Authenticate header specifies the authentication scheme(s) and parameters needed to gain access. Common schemes: Basic (base64-encoded username:password, insecure over HTTP), Bearer (token-based, typically JWT), Digest (hash-based, deprecated), OAuth 2.0 (Bearer tokens with scopes). Client receiving 401 should prompt for credentials or refresh authentication token, then retry with proper Authorization header. Never cache 401 responses. Distinction from related codes: 401 for missing/invalid authentication (‘who are you?’), 403 for insufficient permissions (‘you can’t do this’), 407 for proxy authentication. Best practices: Always use HTTPS for authenticated endpoints, implement token expiration and refresh mechanisms, include error codes for programmatic handling (expired_token, invalid_token, insufficient_scope), rate limit authentication attempts to prevent brute force, never log sensitive credentials or tokens.
Example: Mobile app requests GET /api/notifications. First request has no Authorization header - server returns 401 with WWW-Authenticate: Bearer realm=‘api’. App redirects to login screen, user authenticates, app gets JWT token. Second request includes Authorization: Bearer eyJhbG… - server validates token and returns 200 with notifications. Token expires after 1 hour - next request returns 401 with error=‘expired_token’. App uses refresh token to get new JWT, retries successfully.
Code Example
// Production-grade JWT authenticationconst express = require('express');const jwt = require('jsonwebtoken');const bcrypt = require('bcrypt');const app = express();
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
// Enhanced authentication middlewarefunction authenticate(req, res, next) { const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401) .set('WWW-Authenticate', 'Bearer realm="api", charset="UTF-8"') .json({ error: 'unauthorized', code: 'MISSING_CREDENTIALS', message: 'Authentication required', hint: 'Include Authorization: Bearer <token> header' }); }
const token = authHeader.substring(7); // Remove 'Bearer '
try { const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET); req.user = decoded; next(); } catch (err) { let code, message;
if (err.name === 'TokenExpiredError') { code = 'TOKEN_EXPIRED'; message = 'Access token has expired'; } else if (err.name === 'JsonWebTokenError') { code = 'INVALID_TOKEN'; message = 'Invalid access token'; } else { code = 'AUTHENTICATION_FAILED'; message = 'Authentication failed'; }
return res.status(401) .set('WWW-Authenticate', `Bearer realm="api", error="${code.toLowerCase()}"`) .json({ error: 'unauthorized', code, message }); }}
// Login with passwordapp.post('/api/auth/login', async (req, res) => { const { email, password } = req.body;
const user = await db.getUserByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) { // Don't reveal whether email or password was wrong return res.status(401).json({ error: 'unauthorized', code: 'INVALID_CREDENTIALS', message: 'Invalid email or password' }); }
// Generate tokens const accessToken = jwt.sign( { userId: user.id, email: user.email }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' } );
const refreshToken = jwt.sign( { userId: user.id }, REFRESH_TOKEN_SECRET, { expiresIn: '7d' } );
// Store refresh token in DB await db.saveRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken, expiresIn: 900 });});
// Refresh access tokenapp.post('/api/auth/refresh', async (req, res) => { const { refreshToken } = req.body;
if...Deep Dive
The 401 Unauthorized status code, specified in RFC 9110 Section 15.5.2, indicates that the request has not been applied because it lacks valid authentication credentials for the target resource. Per RFC 9110, the server generating a 401 response MUST send a WWW-Authenticate header field containing at least one challenge applicable to the target resource. The user agent MAY repeat the request with a new or replaced Authorization header field. If the 401 response contains the same challenge as the prior response, and the user agent has already attempted authentication at least once, the user agent SHOULD present the enclosed representation to the user since it usually contains relevant diagnostic information.
Technical Details
RFC 9110 Section 11 (HTTP Authentication) defines the complete authentication framework. The WWW-Authenticate header structure follows the pattern: WWW-Authenticate: <auth-scheme> realm=“<realm>”, <param1>=“<value1>”, <param2>=“<value2>”. Common authentication schemes include Basic (RFC 7617), Bearer (RFC 6750), Digest (RFC 7616, deprecated), OAuth 2.0 (RFC 6749), and Mutual TLS (RFC 8705). Each scheme has specific requirements for credential formatting in the Authorization header.\n\nBasic authentication (RFC 7617) transmits credentials as base64-encoded username:password. Format: Authorization: Basic base64(username:password). Security concerns: credentials sent in every request, easily decoded (not encrypted, just encoded), vulnerable to replay attacks. Use only over HTTPS. Despite security issues, Basic auth is common for internal tools, API testing, and legacy systems. RFC 7617 specifies charset parameter should be UTF-8: WWW-Authenticate: Basic realm=“api”, charset=“UTF-8”.\n\nBearer token authentication (RFC 6750) for OAuth 2.0 and JWT. Format: Authorization: Bearer <token>. WWW-Authenticate: Bearer realm=“api”, error=“invalid_token”, error_description=“Token expired”. Error codes per RFC 6750: invalid_request (missing parameter), invalid_token (token malformed, expired, or revoked), insufficient_scope (token lacks required scopes). JWT tokens (RFC 7519) encode claims as JSON, signed with HMAC or RSA. Validation: verify signature, check expiration (exp claim), validate issuer (iss), audience (aud), and not-before (nbf). Token refresh pattern: short-lived access tokens (15 minutes) + long-lived refresh tokens (7 days) minimize exposure.\n\nDigest authentication (RFC 7616) uses MD5/SHA hashing to avoid sending passwords plaintext. More secure than Basic over HTTP but deprecated in favor of HTTPS + Bearer tokens. Complexity and weak hash algorithms make it obsolete for modern APIs. Historic note: Digest auth addressed password exposure but not replay attacks; nonces provide some protection.\n\nSecurity best practices for 401 responses: Never reveal whether username or password was incorrect - use generic “Invalid credentials” message to prevent user enumeration. Implement rate limiting on authentication endpoints - limit to 5-10 attempts per minute per IP to prevent brute force. Use bcrypt, scrypt, or Argon2 for password hashing with appropriate cost factors. Implement account lockout after repeated failures - temporarily disable account after 5-10 failed attempts. Require MFA for sensitive operations. Use secure session management - regenerate session IDs after login, implement idle timeouts, provide logout functionality. For API keys, use cryptographically random generation (crypto.randomBytes, not Math.random).\n\nToken expiration strategies: Access tokens should be short-lived (15-60 minutes) to limit exposure. Refresh tokens should be long-lived (days to weeks) but revocable. Implement token rotation - issue new refresh…
Code Example
// Enterprise-grade authentication implementation\nconst express = require('express');\nconst jwt = require('jsonwebtoken');\nconst bcrypt = require('bcrypt');\nconst crypto = require('crypto');\nconst { RateLimiterMemory } = require('rate-limiter-flexible');\nconst app = express();\n\nconst ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;\nconst REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;\n\n// Rate limiter for auth endpoints\nconst loginLimiter = new RateLimiterMemory({\n points: 5, // 5 attempts\n duration: 60 * 15, // per 15 minutes\n blockDuration: 60 * 60 // block for 1 hour\n});\n\n// Authentication middleware with comprehensive error handling\nfunction authenticate(options = {}) {\n const { requireScopes = [] } = options;\n \n return async (req, res, next) => {\n const authHeader = req.headers.authorization;\n \n // Missing authentication\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n return res.status(401)\n .set('WWW-Authenticate', 'Bearer realm=\"api\", charset=\"UTF-8\"')\n .json({\n error: 'unauthorized',\n code: 'AUTHENTICATION_REQUIRED',\n message: 'Valid authentication credentials required',\n hint: 'Include Authorization: Bearer <token> header'\n });\n }\n \n const token = authHeader.substring(7);\n \n try {\n const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET, {\n algorithms: ['HS256'], // Prevent algorithm confusion\n issuer: 'api.example.com',\n audience: 'api'\n });\n \n // Check token revocation (blacklist)\n const revoked = await redis.get(\`revoked:${decoded.jti}\`);\n if (revoked) {\n return res.status(401)\n .set('WWW-Authenticate', 'Bearer realm=\"api\", error=\"invalid_token\"')\n .json({\n error: 'unauthorized',\n code: 'TOKEN_REVOKED',\n message: 'Access token has been revoked'\n });\n }\n \n // Scope validation\n if (requireScopes.length > 0) {\n const userScopes = decoded.scopes || [];\n const hasAllScopes = requireScopes.every(s => userScopes.includes(s));\n \n if (!hasAllScopes) {\n return res.status(401)\n .set('WWW-Authenticate', \`Bearer realm=\"api\", error=\"insufficient_scope\", scope=\"${requireScopes.join(' ')}\"\`)\n .json({\n error: 'unauthorized',\n code: 'INSUFFICIENT_SCOPE',\n message: 'Token lacks required scopes',\n required_scopes: requireScopes,\n user_scopes: userScopes\n });\n }\n }\n \n req.user = decoded;\n next();\n } catch (err) {\n let code, error, message;\n \n if (err.name === 'TokenExpiredError') {\n code = 'TOKEN_EXPIRED';\n error = 'invalid_token';\n message = 'Access token has expired';\n } else if (err.name === 'JsonW...Frequently Asked Questions
What's the difference between 401 Unauthorized and 403 Forbidden?
401 means authentication failed - 'Who are you?' The server doesn't know who you are because credentials are missing, invalid, or expired. 403 means authorization failed - 'I know who you are, but you can't access this.' Authentication succeeded but you lack required permissions. Example: Requesting admin endpoint without logging in = 401. Logged in as regular user requesting admin endpoint = 403.
Do I need to include WWW-Authenticate header with 401 responses?
Yes, per RFC 9110, servers generating 401 responses MUST send a WWW-Authenticate header field containing at least one challenge. This tells clients which authentication scheme to use (Basic, Bearer, etc.) and required parameters like realm. Example: WWW-Authenticate: Bearer realm='api', error='invalid_token'. Modern frameworks often handle this automatically.
Should I return 401 for invalid username or invalid password?
Return 401 for both, with the same generic message: 'Invalid credentials.' Never reveal whether the username or password was incorrect - this prevents user enumeration attacks where attackers determine valid usernames. Use constant-time comparison and consistent error messages regardless of which credential failed.
How should clients handle 401 errors?
Don't automatically retry 401 without updating credentials - the error won't resolve itself. For expired tokens: attempt token refresh using refresh token, then retry original request with new access token. If refresh fails or no refresh token exists: redirect to login. Implement token refresh proactively (at 80% of lifetime) to prevent user-facing 401s. Queue concurrent requests during refresh to avoid multiple refresh attempts.
Should API keys use 401 or 403 when invalid?
Use 401 for missing or invalid API keys - they're authentication credentials. Use 403 when API key is valid but lacks required permissions/scopes for the requested operation. Example: GET /api/data with no API key = 401. GET /api/data with valid read-only API key but endpoint requires write scope = 403.
Common Causes
- Missing Authorization header (user not logged in or token not sent)
- Expired JWT access token (token lifetime exceeded, need refresh)
- Invalid or malformed JWT token (signature verification failed)
- Incorrect username or password combination during login
- Revoked or blacklisted authentication token
- Invalid API key or incorrect format (missing ‘Bearer’ prefix)
- Cookies cleared or session expired on server
- Token signed with wrong secret key or algorithm
Implementation Guidance
- Include Authorization: Bearer <token> header in requests requiring authentication
- Implement token refresh logic - use refresh token to get new access token when expired
- Verify JWT token signature using correct secret key and algorithm
- For login failures, check credentials and try again with correct username/password
- Regenerate authentication tokens if server secret key changed
- Use HTTPS for all authenticated endpoints - never send credentials over HTTP
- Implement proper token storage - use httpOnly cookies or secure storage, not localStorage for sensitive tokens
- Handle CORS preflight (OPTIONS) requests without requiring authentication
- Implement rate limiting on auth endpoints to prevent brute force attacks
- Log authentication failures for security monitoring and debugging