How HTTP Works
4xx · Client error
< HTTP/1.1 401 Unauthorized

HTTP 401 Unauthorized: Authentication Required

401 Unauthorized means missing or invalid credentials. Read WWW-Authenticate, check the Authorization header, token expiry and proxies, with fixes by stack.

Reviewed 6 min readbeginner5 sourcesTry itMarkdown
Cacheable
Only with explicit freshness
Retry?
After authenticating (send valid credentials)
Usually sent by
Origin app or API gateway
Spec
RFC 9110 §15.5.2
Often confused with
403
On this page

TL;DR: 401 Unauthorized means you need valid login credentials to access this URL. Read WWW-Authenticate, then check that your Authorization header reaches the server with the expected scheme and an unexpired token for this API.

Despite the name, 401 is about authentication (“who are you?”), not authorization. RFC 9110 section 15.5.2 says the server “MUST generate a WWW-Authenticate header field” containing at least one challenge. If credentials were accepted but the user is not allowed in, the correct code is 403.

The messages and response bodies here are illustrative. Client messages use https://api.example.com/resource; reason phrases and URLs can vary.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"
Content-Type: application/json

{"error":"invalid_token"}

What you see in your client

  • Axios: AxiosError: Request failed with status code 401 with the default status handling. Inspect error.response.data for the server’s explanation. Axios source.
  • fetch: the promise resolves with response.status === 401 and response.ok === false. Check the status before reading the body; HTTP errors do not enter catch automatically. MDN fetch.
  • Python requests: requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.example.com/resource when you call response.raise_for_status(). Requests source.
  • curl -f: curl: (22) The requested URL returned error: 401. Use curl -i without -f while inspecting the error body. curl source.
  • .NET: HttpRequestException with the English message Response status code does not indicate success: 401 (Unauthorized). after EnsureSuccessStatusCode(). Runtime message template.
  • Spring WebClient: WebClientResponseException$Unauthorized: 401 Unauthorized from POST https://api.example.com/resource with retrieve(). Read getResponseBodyAsString(). Spring 6.2 source.
  • Spring RestTemplate (6.2): HttpClientErrorException$Unauthorized: 401 Unauthorized on POST request for "https://api.example.com/resource": [no body] with the default error handler and an empty response body. Read the exception body when present. Message builder, exception subclasses.

Who sent it?

Read the WWW-Authenticate value first; it names the layer’s expectations.

Header seenMeaning
Basic realm="..."Browser shows a login dialog. Often nginx auth_basic, Apache AuthType Basic, or a staging password
Bearer error="invalid_token"Your API validated the token and rejected it (expired, wrong signature, wrong audience). Format is RFC 6750 section 3
Bearer with no errorA Bearer challenge; inspect whether your request included credentials
Negotiate / NTLMWindows or Kerberos auth on IIS or a corporate proxy
No WWW-Authenticate at allThe server is breaking the spec; some gateways (and API Gateway authorizers) do this. Look at Server, Via and X-Amzn-ErrorType: UnauthorizedException to find the layer
Proxy-Authenticate and status 407A forward proxy wants credentials, not the origin. See Proxy-Authenticate

Fix it, most common first

  1. Header never arrives. Log whether it is present and which scheme it uses; keep the token out of logs. Cross-origin browser requests with Authorization trigger a preflight that must list it in Access-Control-Allow-Headers; otherwise the browser never sends it. A cross-origin redirect strips Authorization in fetch; curl also restricts credentials when following redirects. Fetch redirect rules, curl manual.
  2. Wrong scheme or format. The header must be Authorization: Bearer <token> using the usual single-space format. RFC 6750 permits one or more spaces; authentication scheme names are case-insensitive. Common slips: Bearer Bearer abc, a surrounding quote, a literal undefined, a trailing newline from a file read, Token or JWT where the API expects Bearer.
  3. Expired token. Read the JWT payload with a local decoder that supports base64url, and compare exp with date +%s. Refresh once, then retry once; do not loop.
  4. Clock skew. Compare the issuer and validator clocks when exp or nbf fails. Configure the validator’s documented clock tolerance; jsonwebtoken exposes clockTolerance in seconds. Library options.
  5. Wrong audience or issuer. aud/iss mismatch after switching environments, or a token minted for another API.
  6. Proxy strips the header. Some CGI/FastCGI setups drop Authorization. For Apache CGI, check CGIPassAuth On; its default is Off. Apache directive. nginx forwards it by default with proxy_pass, but not if you wrote proxy_set_header Authorization "".
  7. Basic auth credentials. fetch() rejects URLs with embedded credentials (https://user:pass@host) with a TypeError; set the header explicitly.

Common causes by stack

  • FastAPI: an OAuth2 dependency can reject a missing Bearer token with 401 and WWW-Authenticate: Bearer; send the access token, not the refresh token. FastAPI OAuth2 guide.
  • ASP.NET Core JWT bearer: failed signature, issuer, audience or expiry validation produces 401; compare the token claims with Authority and Audience. Microsoft JWT guidance.
  • AWS ALB: an authentication listener with OnUnauthenticatedRequest=deny returns 401 for unauthenticated requests; check that listener action before debugging the target application. ALB troubleshooting.
  • Kubernetes ingress-nginx: nginx.ingress.kubernetes.io/auth-url calls an external auth service; a 401 from nginx’s auth subrequest is passed back with its challenge. Check the auth service logs and the configured URL. Ingress annotations, nginx auth_request.

Fix by stack

Express / Node.js

function requireAuth(req, res, next) {
  const [scheme, token] = (req.headers.authorization ?? '').split(/ +/)
  if (scheme.toLowerCase() !== 'bearer' || !token) {
    return res.status(401).set('WWW-Authenticate', 'Bearer realm="api"').json({ error: 'missing_token' })
  }
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET, { clockTolerance: 30 })
    next()
  } catch (err) {
    const error = err.name === 'TokenExpiredError' ? 'token expired' : 'invalid token'
    res.status(401).set('WWW-Authenticate', `Bearer error="invalid_token", error_description="${error}"`).json({ error })
  }
}

nginx

location /admin/ {
    auth_basic           "Staging";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

Create the file with htpasswd -c /etc/nginx/.htpasswd alice. auth_request delegates the decision to an internal subrequest: a 401 from it becomes a 401 to the client.

Spring Security, Django REST Framework

Spring Security’s BearerTokenAuthenticationEntryPoint produces the RFC 6750 WWW-Authenticate header automatically. Django REST framework uses the first authentication class to decide the challenge: its authenticate_header() must return a value for an unauthenticated denial to use 401; with SessionAuthentication first, it uses 403. Spring JWT authentication, DRF authentication.

Cloudflare Access, API Gateway

Cloudflare Access service tokens use CF-Access-Client-Id plus CF-Access-Client-Secret. In service-token-only mode, failed authentication or authorization returns 401 or 403 instead of a login redirect; check that the application has a matching Service Auth policy. CF-Access-JWT-Assertion is the header Access sends to the origin after authentication. Access service tokens.

For API Gateway REST APIs, UNAUTHORIZED defaults to 401 when a custom authorizer fails authentication; ACCESS_DENIED defaults to 403 when authorization denies the call. Check the gateway response type and authorizer logs. Gateway response types.

Reproduce and verify

curl -sI https://api.example.com/profile                       # expect 401 + WWW-Authenticate
curl -sI -H "Authorization: Bearer $TOKEN" https://api.example.com/profile
curl -sv -u alice:secret https://staging.example.com/ 2>&1 | grep -iE '^(>|<) (authorization|www-auth|HTTP)'

curl -v prints > Authorization: Basic YWxpY2U6c2VjcmV0: Basic is only base64, not encryption, so use HTTPS.

For a failed API call, save the response headers and body separately instead of using only curl -I, which sends HEAD. Use the method that failed: an API can apply different authentication to GET and POST. Read the challenge in headers.txt and the rejection detail in error.json, then compare them with the expected scheme. curl header and output options.

curl -sS -D headers.txt -o error.json \
  -H "Authorization: Bearer $TOKEN" https://api.example.com/profile
grep -i '^www-authenticate:' headers.txt
cat error.json

401 for API designers

  • Always send WWW-Authenticate to name the accepted authentication scheme. error="invalid_token" tells a Bearer client to obtain valid credentials before retrying.
  • Use Bearer error="insufficient_scope" with 403, not 401, for scope problems (RFC 6750 section 3.1).
  • Keep tokens out of URLs and out of localStorage where XSS can read them; an HttpOnly; Secure; SameSite cookie or a backend-for-frontend is safer for browser apps.
  • Return the same authentication failure for an unknown user and a bad password.

401 vs 403, 403 Forbidden, Authorization, WWW-Authenticate, Proxy-Authenticate, authentication guide, 429 for repeated failed logins.

In practice

Express.js
// Auth middleware — return 401 if no valid token
function requireAuth(req, res, next) {
  const token = /^Bearer +([^ ]+)$/i.exec(req.headers.authorization ?? '')?.[1]
  if (!token) {
    return res.status(401)
      .set('WWW-Authenticate', 'Bearer realm="api"')
      .json({ error: 'Authentication required' })
  }
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET)
    next()
  } catch {
    res.status(401)
      .set('WWW-Authenticate', 'Bearer error="invalid_token"')
      .json({ error: 'Invalid or expired token' })
  }
}

app.get('/api/profile', requireAuth, (req, res) => res.json(req.user))
Next.js App Router
// app/api/profile/route.ts
import { verifyToken } from '@/lib/auth'

export async function GET(request: Request) {
  const token = /^Bearer +([^ ]+)$/i.exec(request.headers.get('authorization') ?? '')?.[1]
  if (!token) {
    return Response.json(
      { error: 'Authentication required' },
      {
        status: 401,
        headers: { 'WWW-Authenticate': 'Bearer realm="api"' }
      }
    )
  }
  const user = await verifyToken(token)
  if (!user) {
    return Response.json(
      { error: 'Invalid or expired token' },
      { status: 401, headers: { 'WWW-Authenticate': 'Bearer error="invalid_token"' } }
    )
  }
  return Response.json(user)
}
Django
from django.http import JsonResponse
from functools import wraps
import re

def require_auth(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        header = request.META.get('HTTP_AUTHORIZATION', '')
        match = re.fullmatch(r'Bearer +([^ ]+)', header, flags=re.IGNORECASE)
        token = match.group(1) if match else None
        if not token:
            response = JsonResponse({'error': 'Authentication required'}, status=401)
            response['WWW-Authenticate'] = 'Bearer realm="api"'
            return response
        user = verify_token(token)
        if not user:
            response = JsonResponse({'error': 'Invalid token'}, status=401)
            response['WWW-Authenticate'] = 'Bearer error="invalid_token"'
            return response
        request.user = user
        return view_func(request, *args, **kwargs)
    return wrapper

@require_auth
def profile_view(request):
    return JsonResponse(request.user.to_dict())

Frequently asked questions

What does 401 Authorization Required mean?

It is a server label for HTTP 401: the URL requires valid authentication credentials. Read WWW-Authenticate to see whether the server expects Basic, Bearer or another scheme, then send credentials for that scheme.

What does 401 Unauthorized mean?

The request lacks valid authentication credentials for the target resource, and the response must include a WWW-Authenticate header naming an acceptable scheme (RFC 9110 section 15.5.2). The name is misleading: it concerns authentication, not authorization.

What is the difference between 401 and 403?

401 means the server could not authenticate you, so supplying valid credentials may help. 403 means the server understood the request and refuses it regardless, typically because an authenticated user lacks permission.

Why do I get 401 even though I send a valid token?

Common causes are an expired token or clock skew on the exp or nbf claim, a token issued for a different audience or environment, the wrong scheme (Bearer vs Token), or a proxy or redirect that drops the Authorization header before it reaches the app.

Must a 401 response include WWW-Authenticate?

Yes. RFC 9110 requires at least one challenge in WWW-Authenticate on a 401. For OAuth 2.0 bearer tokens, RFC 6750 adds error values such as invalid_token that tell clients whether to refresh.

Why does my browser show a login popup on a 401?

A 401 with WWW-Authenticate: Basic or Digest makes browsers prompt for credentials. APIs that serve JSON to a browser app should return Bearer challenges, or omit browser-handled schemes, to avoid the popup.

Sources

  1. MDN Web Docs: 401 Unauthorizeddeveloper.mozilla.org
  2. RFC 9110 section 15.5.2: 401 Unauthorizedrfc-editor.org
  3. RFC 9110 section 11: HTTP Authenticationrfc-editor.org
  4. RFC 6750 section 3: The WWW-Authenticate Response Header Fieldrfc-editor.org
  5. nginx: ngx_http_auth_basic_modulenginx.org

Keep going

Browse /search