< HTTP/1.1 401 UnauthorizedHTTP 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.
- 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 yourAuthorizationheader 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 401with the default status handling. Inspecterror.response.datafor the server’s explanation. Axios source. - fetch: the promise resolves with
response.status === 401andresponse.ok === false. Check the status before reading the body; HTTP errors do not entercatchautomatically. MDN fetch. - Python requests:
requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.example.com/resourcewhen you callresponse.raise_for_status(). Requests source. - curl -f:
curl: (22) The requested URL returned error: 401. Usecurl -iwithout-fwhile inspecting the error body. curl source. - .NET:
HttpRequestExceptionwith the English messageResponse status code does not indicate success: 401 (Unauthorized).afterEnsureSuccessStatusCode(). Runtime message template. - Spring WebClient:
WebClientResponseException$Unauthorized: 401 Unauthorized from POST https://api.example.com/resourcewithretrieve(). ReadgetResponseBodyAsString(). 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 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 |
Fix it, most common first
- Header never arrives. Log whether it is present and which scheme it uses; keep the token out of logs. Cross-origin browser requests with
Authorizationtrigger a preflight that must list it inAccess-Control-Allow-Headers; otherwise the browser never sends it. A cross-origin redirect stripsAuthorizationin fetch; curl also restricts credentials when following redirects. Fetch redirect rules, curl manual. - 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 literalundefined, a trailing newline from a file read,TokenorJWTwhere the API expectsBearer. - Expired token. Read the JWT payload with a local decoder that supports base64url, and compare
expwithdate +%s. Refresh once, then retry once; do not loop. - Clock skew. Compare the issuer and validator clocks when
expornbffails. Configure the validator’s documented clock tolerance;jsonwebtokenexposesclockTolerancein seconds. Library options. - Wrong audience or issuer.
aud/issmismatch after switching environments, or a token minted for another API. - Proxy strips the header. Some CGI/FastCGI setups drop
Authorization. For Apache CGI, checkCGIPassAuth On; its default is Off. Apache directive. nginx forwards it by default withproxy_pass, but not if you wroteproxy_set_header Authorization "". - Basic auth credentials.
fetch()rejects URLs with embedded credentials (https://user:pass@host) with aTypeError; 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
AuthorityandAudience. Microsoft JWT guidance. - AWS ALB: an authentication listener with
OnUnauthenticatedRequest=denyreturns 401 for unauthenticated requests; check that listener action before debugging the target application. ALB troubleshooting. - Kubernetes ingress-nginx:
nginx.ingress.kubernetes.io/auth-urlcalls 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-Authenticateto 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
localStoragewhere XSS can read them; anHttpOnly; Secure; SameSitecookie 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, 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
Related
401 vs 403: Authentication vs Authorization
Understand the difference between 401 Unauthorized and 403 Forbidden. Learn when each status code applies, common mistakes, and how to use them correctly in APIs.
407 Proxy Authentication Required: Fix It
407 means your proxy wants credentials before forwarding the request. Fix it in curl, npm, pip, git and browsers, and learn how Proxy-Authenticate works.
HTTP 403 Forbidden: Access Denied
403 Forbidden means the server refuses the request. Find whether Cloudflare, nginx, Apache, S3 or your app sent it; fix permissions, WAF rules and role checks.
419 Page Expired (Laravel CSRF Token Mismatch)
Laravel 419 Page Expired means the CSRF token did not match the session. Causes: expired session, missing @csrf, dropped cookie, cached forms. Fixes included.
561 Unauthorized (AWS ALB Authentication)
AWS ALB 561 means the identity provider returned an error during listener authentication. Read error_reason in access logs and inspect the IdP response.