401 vs 403: missing authentication or a refused request
How HTTP 401 Unauthorized and 403 Forbidden differ under RFC 9110 and the OAuth 2.0 Bearer Token spec: WWW-Authenticate, retry rules, and hiding a resource with 404.
Every API eventually has to decide what to return for a request with no login, and what to return for a logged-in user who isn’t allowed to do what they asked. Under the HTTP specification the first usually gets 401 and the second 403. The two still get swapped a lot, mostly because 401 is named Unauthorized.
This post separates them using RFC 9110, which defines HTTP semantics, and RFC 6750, which defines how OAuth 2.0 bearer tokens are used.
401: no valid authentication credentials
RFC 9110 defines 401 as meaning that the request “has not been applied because it lacks valid authentication credentials for the target resource.” The name says Unauthorized, but the definition is about authentication: whether the requester has proved who they are.
401 comes with one hard requirement. A server generating a 401 response MUST send a WWW-Authenticate header field containing at least one challenge applicable to the target resource. A challenge tells the client which authentication scheme it can retry with. RFC 6750 gives this example:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
A 401 without that header violates the specification.
If the request already carried credentials, a 401 means those credentials were refused, and the client MAY repeat the request with a new or replaced Authorization header. If the user agent has already tried to authenticate at least once and gets another 401 with the same challenge, the specification says it SHOULD show the response body to the user, since the body may contain diagnostic information.
403: understood, but refused
403 means that the server understood the request but refuses to fulfill it. A server that wants to explain why can do so in the response body.
403 does not require WWW-Authenticate. RFC 9110 section 11.6.1 does allow the header on responses other than 401 to indicate that supplying different credentials might change the response.
If the request included credentials, the server considers them insufficient to grant access. The client SHOULD NOT automatically repeat the request with the same credentials, and MAY repeat it with new or different ones.
403 does not always mean “logged in, but lacking permission.” RFC 9110 says a request can be forbidden for reasons unrelated to credentials.
| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| RFC 9110 definition | Lacks valid authentication credentials | Understood the request, refuses to fulfill it |
WWW-Authenticate |
MUST be sent | Not required |
| Retrying | MAY retry with a new Authorization |
SHOULD NOT auto-retry with the same credentials; MAY retry with different ones |
A common way to tell them apart in practice is to ask whether authenticating again could change the outcome. That is a rule of thumb, not a procedure from the RFC. RFC 9110 defines what each code means and says an origin server SHOULD send 401 for a request that omits credentials or carries invalid or partial ones.
With OAuth bearer tokens
RFC 6750 pairs error codes with status codes for resources protected by bearer tokens:
invalid_request: a required parameter is missing or the request is otherwise malformed → 400 (SHOULD)invalid_token: the token is expired, revoked, malformed, or invalid for another reason → 401 (SHOULD)insufficient_scope: the request needs higher privileges than the token provides → 403 (SHOULD)
For invalid_token, the client can obtain a new access token and retry. For insufficient_scope, the server MAY include the required scope. So an expired token gets 401, and a valid token with too little scope gets 403.
When a request carries no authentication information at all, RFC 6750 says the server SHOULD NOT include an error code or other error information. That covers a client that didn’t know authentication was required or tried an unsupported method. The realm-only challenge above is an example.
RFC 6750 dates from 2012, and the RFC Editor lists it as updated by RFC 8996 and RFC 9700. RFC 9700 gives current OAuth 2.0 security guidance but does not change the 401, 403 and insufficient_scope mappings cited here.
Hiding that a resource exists: 404
In its section on 403, RFC 9110 says an origin server that wants to hide the current existence of a forbidden resource MAY respond with 404 instead.
The definition of 404 covers both cases: no current representation was found, or the server isn’t willing to disclose that one exists. A 403 can reveal that something is at the address, which is why the alternative exists.
A 404 is heuristically cacheable unless the method definition or explicit cache controls say otherwise. If you return 404 for a resource that should become visible as soon as permissions change, don’t leave caching to heuristics; choose a policy for the behavior you want. Whether the response may be stored at all and whether shared caches may reuse it are separate decisions, so adding just any Cache-Control value doesn’t settle it. The directives are compared in no-cache vs no-store in Cache-Control.
When a browser may prompt for credentials
The WHATWG Fetch Standard’s HTTP-network-or-cache fetch algorithm has a step that prompts the user for a username and password after a 401. Its outer condition has four parts:
- the response status is 401;
- the response tainting is not
"cors"; - Fetch’s internal
includeCredentialsvalue is true; and - the request has a traversable it can use for user prompts.
includeCredentials doesn’t record whether cookies or an Authorization header were actually sent. It is a boolean Fetch computes: true when the request’s credentials mode is "include", or when the mode is "same-origin" and the response tainting is "basic". A Cross-Origin-Embedder-Policy check can set it back to false, and it can be true even when no credentials went out.
Meeting the outer condition isn’t enough by itself. Inside it, the user is prompted only if the request’s use-URL-credentials flag is unset, or if this fetch is already an authentication fetch. That flag makes a username and password in the URL take precedence, and it is unset by default.
What follows directly from the algorithm is limited. When fetch() makes a CORS request to another origin and the response tainting is "cors", a 401 does not enter this step. The reverse doesn’t hold: meeting every condition doesn’t mean a given browser will show a login dialog. The specification marks multiple or missing WWW-Authenticate headers and parsing issues in this area as “Needs testing,” so actual behavior has to be checked in each browser.
When a proxy needs authentication, it uses 407 Proxy Authentication Required with a Proxy-Authenticate header, not 401.
The usual way to choose
This is not a procedure defined by the RFC; it is the common default when you apply the definitions and recommendations above.
- No credentials, or invalid ones → 401 with
WWW-Authenticate - Identity is known, but this action is not allowed → 403
- You don’t want to reveal that the resource exists → 404 is allowed
403 can also be used for reasons unrelated to credentials. The 3xx codes from the same RFC 9110 are covered in 301, 302, 307 and 308 redirects.
Specifications and documentation used
- RFC 9110 — HTTP Semantics: sections 11.3, 11.4 and 11.6.1 (authentication headers), 15.5.2 (401), 15.5.4 (403), 15.5.5 (404), 15.5.8 (407)
- RFC 6750 — OAuth 2.0 Bearer Token Usage: sections 3 and 3.1 (error codes and status codes)
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 6750 document info (RFC Editor): update relationships
- WHATWG Fetch Standard — HTTP-network-or-cache fetch (Living Standard)
Links and content were checked on October 3, 2026. Fetch is a living standard and changes over time.