# 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.

Source: https://howhttpworks.com/status-codes/401
Last reviewed: 2026-10-05

> **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](https://howhttpworks.com/status-codes/403).

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

```http
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](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **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](https://developer.mozilla.org/en-US/docs/Web/API/Window/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](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **curl -f:** `curl: (22) The requested URL returned error: 401`. Use `curl -i` without `-f` while inspecting the error body. [curl source](https://github.com/curl/curl/blob/master/lib/http.c).
- **.NET:** `HttpRequestException` with the English message `Response status code does not indicate success: 401 (Unauthorized).` after `EnsureSuccessStatusCode()`. [Runtime message template](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring WebClient:** `WebClientResponseException$Unauthorized: 401 Unauthorized from POST https://api.example.com/resource` with `retrieve()`. Read `getResponseBodyAsString()`. [Spring 6.2 source](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java).
- **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](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java), [exception subclasses](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/web/client/HttpClientErrorException.html).

## Who sent it?

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

| Header seen | Meaning |
| --- | --- |
| `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 `error` | A Bearer challenge; inspect whether your request included credentials |
| `Negotiate` / `NTLM` | Windows or Kerberos auth on IIS or a corporate proxy |
| No `WWW-Authenticate` at all | The 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 407 | A forward proxy wants credentials, not the origin. See [Proxy-Authenticate](https://howhttpworks.com/headers/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](https://fetch.spec.whatwg.org/#http-redirect-fetch), [curl manual](https://curl.se/docs/manpage.html#-L).
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](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback).
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](https://httpd.apache.org/docs/2.4/mod/core.html#cgipassauth). 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](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
- **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](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication).
- **AWS ALB:** an authentication listener with `OnUnauthenticatedRequest=deny` returns 401 for unauthenticated requests; check that listener action before debugging the target application. [ALB troubleshooting](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html).
- **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](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#external-authentication), [nginx auth_request](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html).

## Fix by stack

### Express / Node.js

```javascript
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

```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](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html), [DRF authentication](https://www.django-rest-framework.org/api-guide/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](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/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](https://docs.aws.amazon.com/apigateway/latest/developerguide/supported-gateway-response-types.html).

## Reproduce and verify

```bash
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](https://curl.se/docs/manpage.html).

```bash
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.

## Related

[401 vs 403](https://howhttpworks.com/compare/401-vs-403), [403 Forbidden](https://howhttpworks.com/status-codes/403), [Authorization](https://howhttpworks.com/headers/authorization), [WWW-Authenticate](https://howhttpworks.com/headers/www-authenticate), [Proxy-Authenticate](https://howhttpworks.com/headers/proxy-authenticate), [authentication guide](https://howhttpworks.com/guides/authentication), [429](https://howhttpworks.com/status-codes/429) for repeated failed logins.
