The Core Difference
Despite the name, 401 is about authentication (identity), not authorization (permission). 403 is about authorization (permission).
- 401 Unauthorized: The server doesn’t know who you are. You need to provide credentials (log in, send an API key, include a token).
- 403 Forbidden: The server knows who you are, but you don’t have permission to access this resource.
The naming is a historical accident — 401 was named “Unauthorized” but its semantics are about authentication. RFC 7235 clarifies this explicitly.
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 WWW-Authenticate Header
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 include `WWW-Authenticate` — there's no authentication challenge because the problem isn't missing credentials, it's insufficient permissions.
If you return a 401 without `WWW-Authenticate`, you're technically violating the HTTP spec (RFC 7235 §3.1).
## Security: 403 vs 404
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 no token is provided at all, return 401. Reserve 403 for cases where you've verified identity but denied access.
**Returning 401 for permission failures** — if a logged-in user tries to access another user's data, that's 403, not 401. Returning 401 tells them to "try logging in again," which is confusing and incorrect.
**Returning 403 without logging** — 403s from authenticated users are worth logging. They can indicate misconfigured permissions, a bug in your authorization logic, or a user probing for access they shouldn't have.
**Using 401 for rate limiting** — rate limiting is 429 Too Many Requests. Using 401 or 403 for rate limits confuses clients into thinking it's an auth problem.
## 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.