The Fastest Way To Decide
Ask two questions in order:
- does the server know who this client is
- if yes, is this identity allowed to do this
If the answer to the first question is no, return 401. If the answer to the first question is yes but the second is no, return 403.
The Core Difference
Despite the name, 401 Unauthorized is really about authentication. 403 Forbidden is about authorization.
- 401 Unauthorized: the request lacks valid credentials, or the credentials are expired or invalid
- 403 Forbidden: the identity is known, but access is still denied
When Each Applies
| Scenario | Correct Code |
|---|---|
No Authorization header sent | 401 |
| Invalid or expired token | 401 |
| Valid token, but user lacks permission | 403 |
| Admin-only resource accessed by regular user | 403 |
| Resource exists but is hidden from this user | 403 (or 404) |
| IP blocklist | 403 |
| Rate limit exceeded | 429 (not 403) |
The Header That Usually Gives 401 Away
A 401 response must include a WWW-Authenticate header that tells the client how to authenticate:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"
```text
A 403 response does not carry an authentication challenge, because the problem is not "please authenticate." It is "authentication did not unlock this action."
## 403 vs 404 When You Want To Hide Existence
There's a deliberate security pattern of returning 404 instead of 403 for resources that exist but the user shouldn't know about. If your API returns 403 for `/admin/users`, you've confirmed to an attacker that the endpoint exists. Returning 404 reveals nothing.
This is called "security through obscurity" and is a valid defense-in-depth measure for sensitive resources. The tradeoff: it makes debugging harder for legitimate users.
## Browser Behavior
Browsers handle 401 specially: they may show a native authentication dialog (for `Basic` auth challenges) or trigger your app's login flow. A 403 gets no special browser treatment — it's just an error response.
## Common Mistakes
**Returning 403 when credentials are missing** — if the client never authenticated successfully, the server is not at the permission question yet.
**Returning 401 for permission failures** — telling a logged-in user to "authenticate again" when they simply lack access is misleading and makes client behavior worse.
**Using 403 for rate limiting** — rate limiting is `429 Too Many Requests`, not an auth decision.
**Forgetting to log real 403s** — they are often valuable signals for permission bugs or access probing.
## API Design Guidance
For REST APIs:
```http
GET /api/profile → 401 (no token)
GET /api/profile → 200 (valid token, own profile)
GET /api/users/other-id → 403 (valid token, not your profile)
GET /api/admin/dashboard → 403 (valid token, not an admin)
```text
For the 403 case where you want to hide resource existence:
```http
GET /api/users/other-id → 404 (valid token, resource hidden)
Choose 403 when the user should know the resource exists but they can’t access it. Choose 404 when revealing the resource’s existence is itself a security concern.