Walk through the HTTP challenge-response authentication flow: what does a 401 Unauthorized response carrying a WWW-Authenticate header tell the client, and what exactly does the client send back?
answer
- 401 challenges, client re-sends
- WWW-Authenticate -> Authorization
- scheme + realm = protection space
- stateless: credentials every request
- 401 unauthenticated, 403 forbidden
basics
~20 sThe server refuses with 401 and a WWW-Authenticate header naming an authentication scheme, usually plus a realm. The client gets credentials for that scheme and retries the same request with an Authorization header. HTTP is stateless, so the header repeats on every later request.
solid answer
~50 sHTTP authentication is a generic **challenge-response** framework (RFC 9110 section 11). The server declines with **401 Unauthorized** and must include **WWW-Authenticate** — the challenge. That header names an auth **scheme** (`Basic`, `Digest`, `Bearer`, `Negotiate`, ...) and usually parameters, most often `realm="..."`, which labels the protection space the credentials apply to. The client picks a scheme it supports, obtains credentials (a browser prompt, config, a token endpoint), and **re-sends the identical request** with `Authorization: <scheme> <credentials>`. Nothing server-side is implied: HTTP is stateless, so the header is resent on every request into that protection space. Two practical points. Clients normally authenticate **preemptively** — they send `Authorization` on the first request and never see the 401 — so the challenge mostly matters as the failure signal. And 401 means *missing or bad credentials*; **403 Forbidden** means the credentials were understood but this principal is not allowed, so a 403 carries no challenge and retrying with the same credentials is pointless.
code
http · 14 linesGET /reports/2026 HTTP/1.1
Host: api.example.com
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="reports", error="invalid_token"
Content-Length: 0
GET /reports/2026 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/jsongo deeper
Recall the loop cleanly: 401 plus WWW-Authenticate names a scheme, client retries with Authorization, credentials repeat on every request, and 401 is not 403.
Add the mechanics: scheme plus realm grammar, protection space and credential caching, preemptive auth to save a round trip, and why TLS is mandatory.
Talk about operating it: diagnosing prompt loops, 401 without a challenge breaking clients, and why responses to Authorization-bearing requests must not be stored by shared caches.
Frame the choice of framework itself: when native HTTP auth beats cookie or token-in-body schemes, how challenge semantics interact with gateways and CDNs, and what the 401/403/404 disclosure policy should be platform-wide.
## The framework HTTP has no built-in login page and no session. Instead RFC 9110 section 11 (which absorbed RFC 7235) defines a tiny, scheme-agnostic **authentication framework**: the server *challenges*, the client *responds with credentials*, and the server re-evaluates the request. Everything specific — how credentials are computed, whether they expire, whether they are a password or a token — lives in a pluggable **authentication scheme**. Four header fields carry the whole thing: - **WWW-Authenticate** — sent by an origin server on **401 Unauthorized**; one or more challenges. - **Authorization** — sent by the client; credentials for the origin server. - **Proxy-Authenticate** — sent by a proxy on **407 Proxy Authentication Required**. - **Proxy-Authorization** — sent by the client; credentials for the *next* proxy. ## Anatomy of a challenge A challenge is a scheme token followed by either a single opaque `token68` blob or a comma-separated list of `name=value` auth-params: `WWW-Authenticate: Basic realm="ops-console", charset="UTF-8"` The scheme name is **case-insensitive** (`bearer` equals `Bearer`), and so are parameter names. `realm` is the one parameter defined for all schemes: an opaque string naming a **protection space** so a user agent can cache and reuse the right credentials. The protection space is scoped to the origin (scheme + host + port) plus the realm value — credentials cached for one realm must not be sent to another. Realms are shown to humans in browser prompts, so they should be short and non-sensitive, never a stack trace or a username list. ## The response half The client re-sends the *same* request, now with `Authorization: <scheme> <credentials>`. There is no negotiation state kept on the server; a REST/API client typically hardcodes the scheme it uses. If the credentials are rejected, the server answers 401 again — a browser then re-prompts, which is why a loop of prompts usually means bad credentials rather than a broken flow. Because every request carries credentials, **transport security matters**: `Basic` credentials are base64, which is encoding not encryption, and bearer tokens are equally replayable. Send them over TLS only. ## Preemptive authentication Waiting for a 401 costs a round trip and, for non-idempotent requests, means uploading the body twice (or dancing with `Expect: 100-continue`). Most API clients therefore attach `Authorization` from the start. Browsers do the same after the first challenge: once they have credentials for a realm they send them preemptively to URIs at or below the challenged path. So in production the challenge is often visible only when something is wrong. ## 401 versus 403 This is the classic follow-up. **401** = *unauthenticated*: the request lacked usable credentials, and the response describes how to supply them. **403** = *authenticated but not permitted*, or the server refuses to say more; it carries no `WWW-Authenticate` because there is nothing useful to retry. Returning 403 for a missing token robs the client of the challenge; returning 401 for a permission failure invites clients into a retry loop. Some APIs deliberately return 404 instead of 403 to avoid confirming a resource exists — a defensible privacy choice, but a conscious one. ## Caching interaction A request with an `Authorization` header is user-specific, so a shared cache must not reuse its response for other users unless the response explicitly permits it (`public`, `s-maxage`, or `must-revalidate`). Forgetting this is how one user's data ends up served to another from a CDN. ## What breaks in practice - 401 sent **without** `WWW-Authenticate` — a spec violation; well-behaved clients (curl `--anyauth`, HTTP libraries, browsers) then have no idea which scheme to use and cannot prompt. - A challenge naming a scheme the client does not implement — the client must ignore that challenge and, if none is left, treat the response as an error. - Using 401 to mean "session expired" while the app actually uses cookies: the honest signal is still 401, but there is no HTTP-level challenge for cookie login, so teams send `WWW-Authenticate` with a real scheme or accept that the status alone is the contract.
- Must a client wait for a 401 before sending Authorization?No. Clients may authenticate preemptively by attaching the Authorization header to the very first request, and most API clients do. This saves a round trip and avoids re-sending a request body. The challenge then only appears when credentials are missing, expired or wrong.
- What is the realm parameter actually for?It labels a protection space so a user agent knows which cached credentials belong where. The space is the origin plus the realm string, so credentials cached for realm "admin" must not be auto-sent to realm "public" on the same host. It is displayed to users, so keep it short and free of sensitive detail.
- When would you return 403 instead of 401?When the request carried valid credentials but the authenticated principal is not allowed to do this. A 403 carries no WWW-Authenticate because re-authenticating with the same identity will not help. Use 401 strictly for missing, malformed or rejected credentials.
A doorman turns you away and says which kind of pass he accepts and which building wing it is for; you go get that pass and present it every single time you walk through, because he never remembers you.
saying these in an interview costs you the question
- Saying 401 means 'forbidden' and 403 means 'not logged in' — the two are swapped
- Claiming the server keeps an authentication session after the first successful 401 retry; HTTP is stateless and credentials must be resent
- Returning 401 with no WWW-Authenticate header and calling it correct
- Thinking Basic's base64 credentials are encrypted, so TLS is optional
- Believing the client must always trigger a 401 before it may send Authorization