401 Unauthorized

The request lacks valid authentication credentials. Provide credentials and try again.

Quick Reference

Category4xx Client Error
RFCRFC 9110, Section 15.5.2
CacheableNo
RetryableYes — with valid credentials
Required response headerWWW-Authenticate

What 401 Unauthorized Means

HTTP 401 Unauthorized means the request has not been applied because it lacks valid authentication credentials for the target resource. Despite the name “Unauthorized,” this code is fundamentally about authentication, not authorization. The client simply has not proven who it is yet.

RFC 9110 makes this explicit: a 401 response indicates that the request requires user authentication. The server is saying “I don’t know who you are; prove it.” After authentication, the server may still refuse with 403 Forbidden if the authenticated identity lacks the required permissions.

The naming confusion is historical. HTTP/1.0 used “Unauthorized” when the original authors arguably should have used “Unauthenticated.” The name has been preserved for backward compatibility despite being misleading. In practice, developers understand that 401 = “please authenticate” and 403 = “authenticated but not permitted.”

The WWW-Authenticate Header

RFC 9110 requires that a 401 response include a WWW-Authenticate header field. This header tells the client what authentication scheme(s) the server supports and how to provide credentials. Omitting it is technically a spec violation, though clients will still handle the 401 correctly in most cases.

Common WWW-Authenticate values:

# Basic authentication (credentials in Base64)
WWW-Authenticate: Basic realm="Admin Area"

# Bearer token authentication (OAuth 2.0)
WWW-Authenticate: Bearer realm="API"

# Bearer with error details
WWW-Authenticate: Bearer realm="API", error="invalid_token",
  error_description="The access token has expired"

# Digest authentication
WWW-Authenticate: Digest realm="Protected", qop="auth",
  nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"

For APIs using JWT Bearer tokens, the most useful form includes the error type and description so the client can distinguish between a missing token, an invalid token, and an expired token:

WWW-Authenticate: Bearer realm="API",
  error="token_expired",
  error_description="Access token expired at 2024-04-25T12:00:00Z"

Common Causes of 401

Missing credentials: No Authorization header was sent. The client has not attempted authentication at all.

Expired token: A JWT or OAuth token that was valid has passed its expiry time. The client must refresh or re-acquire a token.

Invalid token: The token is malformed, has an invalid signature, or was revoked (logout, password change, security incident).

Wrong token type: Sending a refresh token where an access token is expected, or a session cookie where a Bearer token is required.

Incorrect credentials: A username/password combination that does not match any account. Note: some implementations prefer to return a generic error rather than confirming which part (username vs password) is wrong, to prevent enumeration attacks.

Returning 401 in Common Frameworks

Express.js JWT Middleware

const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const authHeader = req.headers['authorization'];

  if (!authHeader) {
    return res.status(401)
      .set('WWW-Authenticate', 'Bearer realm="API"')
      .json({ error: 'authentication_required', message: 'No credentials provided' });
  }

  const token = authHeader.split(' ')[1]; // "Bearer "

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    const errorType = err.name === 'TokenExpiredError' ? 'token_expired' : 'invalid_token';
    return res.status(401)
      .set('WWW-Authenticate', `Bearer realm="API", error="${errorType}"`)
      .json({ error: errorType, message: err.message });
  }
}

Django REST Framework

# DRF automatically returns 401 with WWW-Authenticate when
# authentication fails. Customize the response:
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

class BearerTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        auth_header = request.headers.get('Authorization')
        if not auth_header:
            return None  # Returns None to try next authenticator
        if not auth_header.startswith('Bearer '):
            raise AuthenticationFailed('Invalid token format')
        token = auth_header[7:]
        # validate token...
        return (user, token)

Browser Behavior on 401

When a browser receives a 401 with WWW-Authenticate: Basic, it shows a native authentication dialog prompting for a username and password. This dialog is distinct from any login forms in the page itself and is provided by the browser. The entered credentials are Base64-encoded and sent in the Authorization header on the retry request.

For Bearer token authentication, browsers do not show a native dialog. The application’s JavaScript must handle the 401 response and redirect to a login page or refresh the token. SPA frameworks typically intercept 401 responses in an Axios interceptor or fetch wrapper and trigger a re-authentication flow.

401 vs Related Status Codes

CodeMeaningCan credentials fix it?
401 UnauthorizedNot authenticatedYes — provide valid credentials
403 ForbiddenAuthenticated but insufficient permissionsOnly with higher-privilege credentials
407 Proxy Authentication RequiredProxy requires authenticationYes — provide proxy credentials via Proxy-Authorization
511 Network Authentication RequiredNetwork gateway requires sign-in (captive portal)Yes — complete the network sign-in

Frequently Asked Questions

Why is 401 called “Unauthorized” when it means “Unauthenticated”?

A naming mistake in the original HTTP/1.0 specification. The code was defined before the authentication/authorization distinction was well-understood in the context of HTTP. Renaming it to 401 Unauthenticated was discussed but rejected because changing the reason phrase would break existing implementations that parse it. The name stays, but the semantics are “not authenticated.”

Is WWW-Authenticate required on every 401?

Technically yes per RFC 9110. In practice, many APIs omit it for Bearer token endpoints because the client already knows what type of credentials to send. For Basic auth, it is essential because the browser uses the header to trigger the native credentials dialog and to identify the protection realm.

Should I return 401 or 403 when a user tries to access someone else’s data?

403 is correct if the user is authenticated. They are not the resource owner, which is a permissions issue, not an authentication issue. Some APIs return 404 instead to avoid revealing whether the resource exists — this is explicitly permitted by RFC 9110 for sensitive resources.

How should SPAs handle 401 responses?

The typical pattern is an Axios/fetch interceptor that listens for 401 responses, attempts a token refresh using a stored refresh token, retries the original request with the new access token, and redirects to the login page if the refresh also fails. This provides seamless session renewal for users without interrupting their workflow.