skip to content

An API rejects a request that carried an `Authorization: Bearer` header. Explain the difference between answering with `WWW-Authenticate: Bearer error="invalid_token"` and `error="insufficient_scope"`, and which HTTP status code belongs with each.

level: middleimportance: must knowfreq 50%

answer

  1. 400 invalid_request / 401 invalid_token / 403 insufficient_scope
  2. no token at all → 401, bare challenge, no error attribute
  3. 401 = get a new token; 403 = same token will never work
  4. include scope="..." on insufficient_scope
  5. wrong mapping → logout storms or infinite refresh loops

basics

~20 s

invalid_token goes with 401: the token is expired, revoked or malformed, so a fresh token may work — clients refresh and retry. insufficient_scope goes with 403: the token is valid but lacks the required privilege, so retrying with the same token is pointless.

solid answer

~50 s

RFC 6750 defines three error codes and pins each to a status: - **400 `invalid_request`** — the request is malformed: no token where one is required in that form, a token sent twice (header plus query), duplicated parameters. - **401 `invalid_token`** — a token was presented but is expired, revoked, structurally bad, wrong issuer or wrong audience. The challenge belongs on the response so the client knows to obtain a new token. - **403 `insufficient_scope`** — the token authenticates fine but does not grant this operation. The challenge should include a `scope` attribute naming what is required. When **no** credentials were sent at all, answer 401 with a bare `WWW-Authenticate: Bearer realm="..."` and **no** `error` attribute — nothing was wrong with a token, because there was no token. This mapping is load-bearing for clients: a 401 drives the refresh-and-retry path, while a 403 must break out of it. Getting it backwards causes infinite refresh loops or users logged out for a permission problem.

code

http · 7 lines
http
GET /v1/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer expired-token

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"
Cache-Control: no-store

go deeper

for a junior

Know the pairing: expired or bad token is 401, valid token without permission is 403, and 401 is the one that means 'get a new token'.

for a middle

Give all three RFC 6750 codes with their statuses, the bare-challenge case for a missing header, and the scope attribute on insufficient_scope.

for a senior

Explain the client control-flow contract and the concrete failure modes of getting it backwards, plus information-disclosure choices between 403 and 404 and keeping error_description safe.

for a principal

Treat the status/challenge mapping as a fleet-wide contract enforced in a shared filter or gateway, with observability on 401/403 rates to catch refresh loops and permission regressions early.

## The challenge header When a bearer-protected resource refuses a request, RFC 6750 says the response carries a `WWW-Authenticate` header naming the `Bearer` scheme plus optional attributes: ``` WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired", error_uri="https://api.example.com/docs/errors" ``` Attributes: `realm` (protection space label), `scope` (space-delimited list of scopes required), `error` (a code from a fixed set), `error_description` (human-readable, ASCII, for developers not end users), `error_uri` (documentation link). `error_description` must never contain sensitive detail — it is attacker-visible — and must not be shown raw in a UI. ## The three error codes and their statuses **`invalid_request` → 400.** The request is malformed at the protocol level rather than the credential being wrong: a required parameter is missing, a parameter is repeated, or more than one carriage method is used at once (for example both the `Authorization` header and an `access_token` query parameter). Because the fault is the request's shape, no amount of re-authenticating helps; the client must be fixed. **`invalid_token` → 401.** A token was supplied but is not usable: expired (`exp` passed), revoked, signature invalid, malformed, issued by an unexpected issuer, or intended for a different audience. 401 is the right status because the meaning is "authenticate again" — a *different* credential could succeed. This is the trigger clients hang their refresh logic on. **`insufficient_scope` → 403.** The token is entirely valid and the caller is authenticated, but the granted scopes (or the subject's permissions) do not cover this operation. 403 means "I know who you are and the answer is still no". The response should include `scope="orders.write"` so a client capable of stepping up can request that scope. Repeating the request with the same token will never succeed, so the client must not retry blindly. **No credentials at all → 401, no `error`.** If the request carried no `Authorization` header, respond `401` with `WWW-Authenticate: Bearer realm="api"` and omit `error`: the spec reserves `error` for describing a problem with something the client actually sent. This bare challenge is also how a client discovers the scheme in the first place. ## Why the 401/403 split matters operationally Almost every HTTP client library, SDK and interceptor implements the same rule: *on 401, refresh the token once and retry; on 403, surface the failure*. Two classic bugs come from breaking that contract. 1. **Returning 403 for an expired token.** Silent refresh never fires. The user is bounced to a login screen, or a background job dies, even though a refresh would have fixed it in one round trip. Support tickets read "randomly logged out". 2. **Returning 401 for a missing scope.** The client refreshes, receives a token with exactly the same scopes, retries, gets 401, refreshes again — an infinite loop hammering the authorization server, often ending in rate limiting or account lockout. In the worst version the client also clears the session on repeated failure, so a missing permission logs the user out. So the mapping is not pedantry; it is the API's half of a control-flow contract with every client. ## Design guidance - **Always emit `WWW-Authenticate` on a 401.** RFC 9110 requires it, and clients use it to learn the scheme. Returning a bare 401 with a JSON body is a common gap. - **Do not leak.** Do not distinguish "no such user" from "wrong token" in `error_description`, and do not echo the token back. Keep descriptions generic and put detail in server-side logs correlated by request id. - **Watch information disclosure on 403.** Sometimes revealing that a resource exists but you lack permission is itself a leak; then a 404 is the deliberate choice. That is a considered exception, not a default. - **Line up the body with the status.** A JSON error body is fine and useful, but it must agree with the status code and the challenge header — clients branch on the status, humans read the body. - **Do not invent statuses.** 419, 440 and similar are non-standard and break intermediaries and client libraries. - **Handle the token-lifetime edge.** If the token is valid but the *user's* permissions were revoked, that is 403 `insufficient_scope` territory, not 401 — the credential is fine, the authorization is not. ## Quick reference | Situation | Status | error attribute | |---|---|---| | No `Authorization` header | 401 | *(none)* | | Malformed request / two carriage methods | 400 | `invalid_request` | | Expired, revoked, bad signature, wrong audience | 401 | `invalid_token` | | Valid token, missing scope or permission | 403 | `insufficient_scope` + `scope="..."` |

  • What should the response look like when a request arrives with no Authorization header at all?
    A 401 carrying `WWW-Authenticate: Bearer realm="api"` with no `error` attribute — the attribute is reserved for describing a problem with a credential the client actually sent, and here none was sent. That bare challenge is also how a client discovers which scheme the resource expects.
  • Why do client libraries care so much about the 401-versus-403 distinction here?
    Nearly every HTTP client implements refresh-on-401 and surface-on-403. Returning 403 for an expired token stops silent refresh and logs users out spuriously; returning 401 for a missing scope makes clients refresh, retry with an identical token and loop forever, hammering the authorization server. The status is effectively part of the API contract.
  • Is it ever right to return 404 instead of 403 for a valid token lacking permission?
    Yes, when merely confirming that a resource exists is itself a disclosure — for example private repositories or another tenant's records. In that case 404 is a deliberate information-hiding choice, applied consistently, and documented; it must not be the accidental default, since it removes the client's ability to distinguish a permission problem from a bad URL.

saying these in an interview costs you the question

  • Returning 403 for an expired token, breaking silent refresh
  • Returning 401 for a scope problem, causing infinite refresh loops
  • Returning a 401 with no WWW-Authenticate header at all
  • Inventing non-standard statuses such as 419 or 440 for expiry
  • Putting sensitive or overly specific detail in error_description, or echoing the token back

context