breakitdown

401 Unauthorized vs 403 Forbidden

Answer

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.

401
Unauthorized RFC 9110 §15.5.2

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.

403
Forbidden RFC 9110 §15.5.4

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

  1. For a 401, confirm the Authorization header actually reaches the app: some proxies and CDNs strip it, and some clients drop it when following a redirect to another host.
  2. Decode the token (for a JWT, check exp, aud and iss) and confirm it was issued for this environment, not staging.
  3. Read the WWW-Authenticate response header; many servers put the reason there, such as error="invalid_token".
  4. For a 403 on a static site, check file ownership and permissions and whether the directory has an index file.
  5. 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

Related explainers

← All HTTP status codes