HTTP 401 vs 403: what is the difference?
Both mean “you can’t have this”, and the names are famously confusing. The real difference is whether logging in again could help.
The difference in one table
| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| Question the server is answering | Who are you? | Are you allowed? |
| Typical cause | Missing, expired or malformed token, wrong password | Valid login but wrong role, IP blocked, file permissions, firewall rule |
| Will logging in again help? | Usually yes | No, unless you log in as someone else |
| Must include | A WWW-Authenticate header saying how to log in | Nothing special |
| What the client should do | Refresh the token or ask the user to sign in | Show “no access”; do not retry in a loop |
The name “Unauthorized” is a historical accident. In HTTP, authentication is proving who you are and authorization is what you may do; 401 is about the first and 403 about the second. The current HTTP standard, RFC 9110, keeps the old names for compatibility.
Which one should my API return?
- No
Authorizationheader, a token that fails its signature check or has expired: 401. - A valid token for a user who lacks the role or owns a different account: 403.
- A resource whose very existence is private, such as someone else’s private repository: 404. It hides that the thing exists at all, which is what many large sites do.
- Too many requests: 429, not 403, ideally with
Retry-After. - A proxy asking for its own login: 407 Proxy Authentication Required.
Looking up a code you don’t recognise? Anvil’s free HTTP status lookup gives the meaning, common causes and retry advice for every status code, including nginx 499 and Cloudflare 520–526.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"
With WWW-Authenticate: Basic, browsers pop up their own username and password box. That is why a 401 from a page, rather than an API call, can suddenly show a login prompt.
Debugging a 401
- Look at the token. Paste a JWT into the JWT decoder and check
exp(expired?),aud(meant for this API?) andiss. It decodes in your browser, so the token is not sent anywhere. - Check the header shape. It must be
Authorization: Bearer <token>with one space and no quotes. Basic auth isBasicplus Base64 ofuser:password; the Base64 tool shows what you are really sending. - Redirects drop it. Many HTTP clients remove the
Authorizationheader when following a redirect to another host, andhttp://tohttps://redirects can do the same. - Clock skew. A server whose clock is a few minutes off can treat a fresh token as expired or not yet valid.
Debugging a 403
- “403 Forbidden” from nginx or Apache on a static site usually means the web server user cannot read the file or enter a directory, or there is no index file and directory listing is off. See chmod 755 explained.
- A CDN or firewall page (often with a reference or ray ID) means a security rule blocked the request before it reached the app. Unusual user agents, VPN addresses and request bodies that look like attacks are common triggers.
- Cloud storage returns 403 for both “no permission” and, often, “this object does not exist”, so check the path before the policy.
- CSRF protection in many frameworks answers a form post with a missing or stale token with 403.
Related
Designing share links you can revoke · Removed someone but they can still see the file · HTTP 429 Too Many Requests · download won't resume
Questions
Is 401 or 403 correct for an expired token?
401. The credentials are no longer valid, and getting a new token fixes it. Use 403 only when a valid identity lacks permission.
Should I return 403 or 404 for a resource the user cannot see?
If revealing that it exists would leak information, return 404. If the user knows it exists and simply lacks permission, 403 is clearer.
Why does my browser show a login popup?
The server returned 401 with WWW-Authenticate: Basic (or Digest). Browsers answer that with their built-in sign-in box.
Is a 403 always the server's fault?
No. It is a deliberate refusal. It may be a misconfiguration, such as file permissions or a firewall rule, but it can also be working exactly as intended.
Published · updated .