401 Unauthorized means the request has no valid credentials: the client is unauthenticated. 403 Forbidden means the credentials are fine but the action is not permitted: the client is authenticated but unauthorised. The names are the wrong way round, which is why the question keeps coming up.
401 Unauthorized
- Who are you? I do not know.
- No token, expired token, bad signature, wrong password.
- Must send
WWW-Authenticate. - Fix: authenticate (log in, refresh the token) and retry.
403 Forbidden
- I know who you are. You may not do this.
- Missing role, wrong tenant, IP blocked, resource owned by someone else.
- No authentication header required.
- Fix: get permission. Retrying with the same identity will not help.
The rule
Ask one question: would a different set of credentials change the answer? If the client could succeed by logging in, or by presenting a valid token, return 401. If the client is already identified and would need a change in permissions rather than in credentials, return 403.
What RFC 9110 says
Section 15.5.2: a 401 “indicates that the request has not been applied because it lacks valid authentication credentials for the target resource”, and the server “MUST send a WWW-Authenticate header field containing at least one challenge”. Section 15.5.4: a 403 “indicates that the server understood the request but refuses to fulfill it”, and “a request might be forbidden for reasons unrelated to the credentials”. It also notes that a server that wishes to hide the existence of a resource may respond with 404 instead of 403.
Examples
| Situation | Code | Why |
|---|---|---|
No Authorization header on a protected API | 401 | No credentials at all. |
| JWT has expired | 401 | Credentials are no longer valid; a refreshed token would succeed. |
| JWT signature does not verify | 401 | Not valid credentials. |
Valid token, user has role viewer, endpoint requires admin | 403 | Identity is established; permission is missing. |
| Valid token, resource belongs to another account | 403 (or 404 to hide existence) | Authenticated but not allowed. |
| Request from a blocked IP range | 403 | Refusal unrelated to credentials. |
| Correct API key, but the plan does not include this feature | 403 (some APIs use 402) | Known client, not entitled. |
| Wrong password at login endpoint | 401 | Failed authentication attempt. |
| Account is locked after too many attempts | 403 or 423 | Authentication will not be attempted; not a credentials problem the client can fix now. |
Headers that go with 401
The challenge header names the scheme the client should use. For token APIs the value is Bearer, optionally with the realm and an error code from RFC 6750:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"
Content-Type: application/json
{"error": "invalid_token", "message": "The access token expired"}
Browsers treat a WWW-Authenticate: Basic challenge specially by showing a login dialog. If you do not want that dialog for an XHR/fetch call, use Bearer or a custom scheme.
Why the distinction matters in practice
- Clients react differently. A well-written client refreshes its token or redirects to login on 401, and shows a “you don’t have access” message on 403. Returning 403 for an expired token traps the user in a permissions error they cannot resolve.
- Monitoring. A spike in 401s usually means an auth outage or expired keys; a spike in 403s means an access-control regression or an attack. Mixing them hides both.
- Security. Answering 403 for a resource that exists and 404 for one that does not lets an attacker enumerate resources. Pick one behaviour for unauthorised access to private resources and apply it consistently; 404 is the safer default when existence is sensitive.
Common mistakes
- Returning 403 when the token is missing. That is a 401.
- Returning 401 without
WWW-Authenticate. Most clients tolerate it, but it violates the specification and confuses HTTP libraries that implement challenge handling. - Returning 200 with
{"error": "unauthorized"}. Status codes exist so that proxies, SDKs and dashboards can act without parsing bodies. - Using 403 for CSRF failures and rate limits. CSRF failure is a 403; rate limiting is a 429.
See the full 4xx status code list for the neighbouring codes.