401 Unauthorized vs 403 Forbidden
Return 401 when the request has no valid credentials, so authenticating could fix it. Return 403 when the credentials are valid (or irrelevant) but the caller is not allowed to do this, so logging in again will not help.
The request lacks valid authentication; the client needs to log in or send a valid credential.
Despite the name it is about authentication (who you are), not permission. The server must include a WWW-Authenticate header naming the scheme it accepts, such as Bearer or Basic. Common causes are a missing or expired token, a token sent to the wrong environment, or a proxy that strips the Authorization header.
The server understood who you are (or does not care) and refuses the request.
Logging in again will not help: the identity is known but lacks permission, or the resource is blocked for everyone. Typical causes include file permissions the web server cannot read, a missing index.html with directory listing disabled, IP or country blocks, WAF rules, and an S3 bucket policy that denies access. Servers may return 404 instead to avoid revealing that the resource exists.
Explained
The names are misleading. 401 "Unauthorized" is really about authentication: the server could not identify you. 403 "Forbidden" is about authorization: the server knows who you are, or does not need to, and refuses anyway.
A 401 must come with a WWW-Authenticate header that names the scheme the server accepts, for example WWW-Authenticate: Bearer. A browser that sees Basic in that header shows its built-in login prompt, which is why some APIs that never want that prompt avoid sending Basic.
A 403 tells the client that retrying with the same identity is pointless. For a user with the wrong role, that is the right answer. If you do not want to reveal that a private resource exists at all, you can return 404 instead, as GitHub does for private repositories.
How to debug it
- For a 401, confirm the
Authorizationheader actually reaches the app: some proxies and CDNs strip it, and some clients drop it when following a redirect to another host. - Decode the token (for a JWT, check
exp,audandiss) and confirm it was issued for this environment, not staging. - Read the
WWW-Authenticateresponse header; many servers put the reason there, such aserror="invalid_token". - For a 403 on a static site, check file ownership and permissions and whether the directory has an index file.
- For a 403 from a CDN or WAF, look for a block ID in the response body and search the provider's firewall events for it.
Gotchas
- Returning 403 for an expired token breaks clients that refresh tokens only on 401.
- Returning 401 without
WWW-Authenticateviolates the spec, and some HTTP clients will not retry with credentials. - A 403 from S3 often means the object does not exist and the caller lacks
s3:ListBucket; S3 hides the 404 in that case.
Related explainers
- 404 Not FoundA 404 means the server has nothing at that URL. Why routes you just deployed return 404, when to use 410...
- 429 Too Many Requests429 means you hit a rate limit. How to read Retry-After, back off correctly with jitter, and find which...
- 405 Method Not Allowed405 means the URL exists but rejects that HTTP method. Common causes: missing routes, trailing-slash...