---
title: "401 Unauthorized vs 403 Forbidden"
url: https://erroneo.us/developers/http-401-vs-403/
description: "401 means authenticate and try again; 403 means credentials will not help. The RFC 9110 rules, OAuth bearer-token cases, and when 404 is the safer answer."
type: guide
section: "Developers"
date_modified: 2026-10-22
publisher: erroneo.us
license: All rights reserved. Quote with attribution and a link.
---

# 401 Unauthorized vs 403 Forbidden

> Two codes, one question: would logging in (again) change the answer? If yes, send 401. If no, send 403. Here is the spec behind that rule and the edge cases that trip people up.

401 and 403 are the most commonly swapped pair of status codes on the web, partly because their names are misleading. "Unauthorized" sounds like a permissions problem, but in HTTP it is about *authentication*: who you are. "Forbidden" is about *authorization*: what you are allowed to do. Once you separate those two ideas, choosing the code is easy.

## What the spec actually says

[RFC 9110 §15.5.2](https://www.rfc-editor.org/rfc/rfc9110#name-401-unauthorized) defines 401 as a request that "has not been applied because it lacks valid authentication credentials for the target resource." It adds a hard requirement: the server "MUST send a WWW-Authenticate header field … containing at least one challenge." A 401 is therefore an invitation: here is how to authenticate, please try again.

[RFC 9110 §15.5.4](https://www.rfc-editor.org/rfc/rfc9110#name-403-forbidden) defines 403 as the server having "understood the request but refuses to fulfill it." If credentials were provided, "the server considers them insufficient to grant access," and the client "SHOULD NOT automatically repeat the request with the same credentials." It also notes that a request "might be forbidden for reasons unrelated to the credentials," such as an IP block.

[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403) sums it up neatly: for 403, "authenticating or re-authenticating makes no difference."

|  | `401 Unauthorized` | `403 Forbidden` |
|---|---|---|
| Question it answers | Who are you? | You are not allowed to do this |
| Concept | Authentication | Authorization (or a policy block) |
| Required header | `WWW-Authenticate` (MUST) | None |
| Will logging in help? | Yes | No |
| Client should | Obtain or refresh credentials, retry | Stop, or try *different* credentials |
| Typical trigger | Missing, expired, revoked or malformed token or session | Wrong role, missing scope, IP/WAF block, file permissions |

## The decision rule

1. **No credentials at all?** Send `401` with a challenge.
2. **Credentials present but invalid or expired?** Send `401`. The client should log in again or refresh its token.
3. **Credentials valid, but this identity may not do this?** Send `403`.
4. **Should this caller even learn the resource exists?** If not, send `404` instead of 403. RFC 9110 allows exactly this: a server that wishes to "hide" a forbidden resource "MAY instead respond with a status code of 404."

## OAuth and bearer tokens

Most modern APIs use bearer tokens, and [RFC 6750 §3.1](https://www.rfc-editor.org/rfc/rfc6750#section-3.1) spells out the mapping precisely:

| Error code in `WWW-Authenticate` | Meaning (RFC 6750) | Status |
|---|---|---|
| `invalid_request` | Missing or duplicated parameter, otherwise malformed | `400` |
| `invalid_token` | Token expired, revoked, malformed or otherwise invalid | `401` |
| `insufficient_scope` | Token valid but needs higher privileges | `403` |
| (no error code) | Request had no authentication information at all | `401` |

```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example", error="invalid_token",
                  error_description="The access token expired"
```

```
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="example", error="insufficient_scope",
                  scope="invoices:write"
```

RFC 6750 also says that when a request has no authentication information, the server "SHOULD NOT include an error code or other error information," so the minimal challenge is just `WWW-Authenticate: Bearer realm="example"`.

## Wrong vs right

Wrong: ~~Logged-out user requests `/account` → `403 Forbidden`~~

Right: `401 Unauthorized` + `WWW-Authenticate` (API), or a `302`/`303` to the login page (browser app)

Why: A 403 tells the client that logging in will not help, so well-behaved clients will not try. For HTML pages, many sites redirect to a login form instead; that is fine as long as API endpoints still return a real 401.

Wrong: ~~Expired JWT → `403 Forbidden`~~

Right: Expired JWT → `401` with `error="invalid_token"`

Why: Client libraries commonly use a 401 as the trigger to refresh the token and retry. A 403 breaks that flow and logs users out for no reason.

Wrong: ~~`401` with no `WWW-Authenticate` header~~

Right: `401` always carries at least one challenge

Why: It is a MUST in RFC 9110. If you genuinely have no challenge to offer (for example a cookie-session web app), consider whether a redirect to login or a 403 better describes the situation.

Wrong: ~~Regular user tries `DELETE /admin/users/42` → `401`~~

Right: `403 Forbidden`, with a body that says the action needs the admin role

Why: The user is authenticated; asking them to authenticate again is a loop. MDN's own [403 example](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403) is exactly this case.

## Common causes of a 403 you did not expect

If you are seeing 403s and your permission logic looks correct, the refusal often comes from somewhere other than your application code:

| Cause | Where to look |
|---|---|
| Web server cannot read the file or directory | File ownership and permissions; nginx/Apache error log |
| Directory with no index file and listing disabled | Missing `index.html`; nginx logs a "directory index of … is forbidden" error |
| WAF, bot protection or CDN rule | CDN or firewall dashboard; response headers that name the vendor |
| IP allowlist or geo-block | Server `allow`/`deny` rules, cloud security groups |
| CSRF check failed | Framework logs; [Django, for example, returns a 403](https://docs.djangoproject.com/en/5.2/ref/csrf/) when the CSRF token is missing or wrong |
| Signed URL expired or signature mismatch | Object storage error body (for example S3's `AccessDenied`) |
| API rate limit reported as 403 | Some APIs do this: [GitHub returns 403 or 429](https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api) for primary rate limits, with `x-ratelimit-remaining: 0` |

## How to debug

```
# See the status line and the challenge header
curl -i https://api.example.com/v1/me

# Same request with a token: did the status change?
curl -i -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/me
```

If the code is `401` without the token and still `401` with it, the token itself is the problem: check expiry, audience, issuer and clock skew. If it moves from `401` to `403`, authentication works and the issue is permissions or scope. If it is `403` both times, the block is probably happening before your auth layer runs (WAF, IP rules, file permissions).

### If you are a visitor

- Sign out and back in, or clear cookies for the site, to fix a stale session (that turns a 401 into a 200).
- For a 403, check you are signed in with the right account; a work and a personal account are easy to mix up.
- Turn off a VPN or proxy briefly. IP-based blocks and bot protection often show up as 403.
- If none of that helps, the site has decided you may not see the page. Contact its owner.

## SEO notes

Google's documentation says [4xx codes other than 429 have no effect on crawl rate](https://developers.google.com/search/docs/crawling-indexing/http-network-errors), and that indexed URLs returning 4xx are removed from the index. It specifically says: "Don't use 401 and 403 status codes for limiting the crawl rate." If you want crawlers to slow down, that is a job for [429](https://erroneo.us/developers/http-429-too-many-requests/) or 503, not an authentication error.

>

Whatever you choose, put a machine-readable explanation in the body. [RFC 9457 problem details](https://www.rfc-editor.org/rfc/rfc9457) (`application/problem+json`) give you standard `type`, `title`, `status` and `detail` fields, which saves every client from guessing what "Forbidden" meant this time.

## From the Error of the Day

1. Oct 22 Developers[CORS errors: the server answered, the browser said no](https://erroneo.us/daily/cors-error/) A CORS error means the browser refused to hand your script the response. The server often answered fine. Fix it with server headers, not frontend hacks.

## Frequently asked questions

**What is the difference between 401 and 403?**

401 Unauthorized means the request lacks valid authentication credentials, and the server must send a WWW-Authenticate challenge. 403 Forbidden means the server understood the request and refuses it; if credentials were sent, they are insufficient, and retrying with the same ones will not help.

**Why is 401 called Unauthorized if it is about authentication?**

It is a historical naming quirk. The name dates from early HTTP, but the definition in RFC 9110 is clearly about missing or invalid authentication credentials, not about permissions.

**Should an expired token return 401 or 403?**

401. RFC 6750, which defines OAuth 2.0 bearer tokens, says an expired, revoked or malformed token is invalid_token and the server should respond with 401. A valid token that lacks the needed scope is insufficient_scope and gets 403.

**Is it OK to return 404 instead of 403?**

Yes, when revealing that a resource exists would itself leak information. RFC 9110 explicitly allows a server that wants to hide a forbidden resource to respond with 404. GitHub does this for private repositories.

**Does a 401 or 403 hurt SEO?**

Google says 4xx codes other than 429 do not affect crawl rate, but indexed URLs that return 4xx are removed from the index. Google also warns not to use 401 or 403 to limit crawl rate.

## Sources

1. [RFC 9110 §15.5.2: 401 Unauthorized](https://www.rfc-editor.org/rfc/rfc9110#name-401-unauthorized)
2. [RFC 9110 §15.5.4: 403 Forbidden](https://www.rfc-editor.org/rfc/rfc9110#name-403-forbidden)
3. [RFC 9110 §11.6.1: WWW-Authenticate](https://www.rfc-editor.org/rfc/rfc9110#name-www-authenticate)
4. [RFC 6750: OAuth 2.0 Bearer Token Usage, §3.1 Error Codes](https://www.rfc-editor.org/rfc/rfc6750#section-3.1)
5. [MDN: 403 Forbidden](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403)
6. [MDN: 401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401)
7. [GitHub Docs: Troubleshooting the REST API](https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api)
8. [Google Search Central: How HTTP status codes affect Google Search](https://developers.google.com/search/docs/crawling-indexing/http-network-errors)
9. [RFC 9457: Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457)
10. [Django documentation: Cross Site Request Forgery protection](https://docs.djangoproject.com/en/5.2/ref/csrf/)

---
Source page: https://erroneo.us/developers/http-401-vs-403/ · More: https://erroneo.us/llms.txt
