skip to content

Walk through how HTTP Digest Authentication (RFC 7616) authenticates a request: what the server sends in its WWW-Authenticate challenge, what the client puts in the Authorization header, and how the `response` hash is computed.

level: middleimportance: should knowfreq 30%

answer

  1. A1 = user:realm:password
  2. A2 = method:uri
  3. H(H(A1):nonce:nc:cnonce:qop:H(A2))
  4. server stores H(A1), not bcrypt
  5. auth-int covers body, nobody ships it

basics

~20 s

The server answers 401 with WWW-Authenticate: Digest carrying realm, a server-generated nonce, qop and algorithm. The client replies with Authorization: Digest containing username, realm, uri, nonce, nc, cnonce, qop and a response hash built from the password plus the method and URI. The password itself never travels.

solid answer

~40 s

Digest is a challenge-response scheme. The server returns `401` with `WWW-Authenticate: Digest realm="...", qop="auth", algorithm=SHA-256, nonce="...", opaque="..."`. The client builds two strings: **A1 = username:realm:password** and **A2 = method:request-uri**, then with `qop=auth` computes ``` response = H( H(A1) : nonce : nc : cnonce : qop : H(A2) ) ``` and retries the request with `Authorization: Digest username=..., realm=..., uri=..., nonce=..., nc=00000001, cnonce=..., qop=auth, response="..."`. The server recomputes the same hash from its stored H(A1) and compares. What that buys: the cleartext password never crosses the wire; the method and URI are folded into the hash so a captured credential cannot be replayed against a different endpoint; `nc` and `cnonce` give per-request freshness. `qop=auth-int` also hashes the body but is almost never implemented. RFC 7616 replaces MD5 with SHA-256 as the recommended algorithm.

code

http · 17 lines
http
GET /private/ HTTP/1.1
Host: api.example.org

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="[email protected]",
    qop="auth", algorithm=SHA-256,
    nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
    opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"

GET /private/ HTTP/1.1
Host: api.example.org
Authorization: Digest username="mufasa", realm="[email protected]",
    uri="/private/", algorithm=SHA-256,
    nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
    nc=00000001, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ",
    qop=auth,
    response="753927fa0e85d155564e2e272a28d1802ca10daf449"

go deeper

for a junior

Know the shape: 401 challenge with a nonce, client answers with a hash instead of the password. Naming realm, nonce and response is enough.

for a middle

Be able to write A1, A2 and the qop=auth response formula from memory and explain why method and URI are inside the hash.

for a senior

Add the operational consequences: shared nonce state across a server farm, URI rewriting by proxies, and the H(A1) storage requirement that rules out bcrypt.

for a principal

Frame it as a design artefact of the pre-TLS era and argue when, if ever, it still belongs in an architecture — typically constrained embedded devices without a TLS story.

## Why it exists Basic Authentication sends `base64(user:password)` on every request, so anyone reading the bytes recovers the password. Digest Authentication (RFC 2617, modernised by **RFC 7616**) was designed for the pre-TLS web: prove you know the password without ever sending it, by sending a keyed hash over values the server chose. ## The challenge An unauthenticated request gets `401 Unauthorized` plus a `WWW-Authenticate` header with scheme `Digest` and these parameters: - **realm** — a label naming the protection space; it is mixed into the hash, so the same password in two realms yields different digests. - **nonce** — an opaque server-generated value, typically encoding a timestamp plus a MAC keyed with a server secret so the server can validate and age it without storing it. - **qop** (quality of protection) — `auth`, or `auth-int` which also covers the body. - **algorithm** — `SHA-256`, `SHA-512-256`, legacy `MD5`, or a `-sess` variant. - **opaque** — a value the client must echo back unchanged. ## The response The client (a browser prompts the user; an API client uses configured credentials) computes: - **A1 = `username:realm:password`**. For the `-sess` variants A1 = `H(username:realm:password):nonce:cnonce`, which rekeys per session. - **A2 = `method:request-uri`** for `qop=auth`; for `qop=auth-int` it is `method:request-uri:H(entity-body)`. - **response = H( H(A1) : nonce : nc : cnonce : qop : H(A2) )** where H is the chosen hash. The legacy RFC 2069 form with no `qop` is `H(H(A1):nonce:H(A2))` — no client nonce, no counter, and therefore much weaker. The client resends the original request with an `Authorization: Digest` header repeating username, realm, the server's nonce and opaque, the `uri` it used, its own `cnonce`, an 8-hex-digit request counter `nc`, the selected `qop`, and the computed `response`. ## Verification The server needs H(A1) — either the password in cleartext or a stored `H(username:realm:password)`. It recomputes the digest with the nonce it issued, the nc/cnonce/uri/method from this request, and compares in constant time. A mismatch is another `401`. ## What is and is not protected The method and the request URI are bound into A2, so a digest captured for `GET /public` cannot authorise `DELETE /admin`. The body is *not* covered unless `qop=auth-int`, and support for that is so rare it is effectively unavailable — so with plain `auth`, an active attacker on a cleartext connection can swap the body of a POST while keeping a valid credential. Nothing else about the exchange is confidential: headers, body and response all travel in the clear unless TLS is used. ## Practical notes The `uri` parameter must match what the server sees, which breaks when a proxy or gateway rewrites the path. Server farms must share nonce state or make nonces stateless via a shared secret. Because verification needs H(A1), Digest is incompatible with modern password storage such as bcrypt or Argon2 — the stored H(A1) is password-equivalent for that realm. Those constraints, plus universal TLS, are why the scheme is now rare outside embedded devices, printers, IP cameras and some SIP deployments.

  • What does `qop=auth-int` add, and why is it almost never used?
    With `auth-int`, A2 becomes `method:uri:H(body)`, so the request body is covered by the digest and cannot be tampered with. It is rarely implemented because the client must buffer the whole body to hash it before sending, which breaks streaming and chunked uploads, and most servers and HTTP stacks simply never added support. In practice you get integrity from TLS instead.
  • Why must the server store H(username:realm:password) rather than a bcrypt hash?
    Verification requires recomputing the exact digest, which needs H(A1) as an input, so the server must be able to derive it. A slow, salted password hash like bcrypt or Argon2 is deliberately not reversible into H(A1), so it cannot be used. The consequence is that a leaked Digest credential table is password-equivalent for that realm and is not protected by any work factor.

saying these in an interview costs you the question

  • Saying the password is sent hashed — it is a hash over the password plus server-chosen values, not a password hash sent in place of the password
  • Claiming Digest makes TLS unnecessary; it encrypts nothing and protects only the credential
  • Thinking `cnonce` comes from the server — the client generates it
  • Assuming the body is authenticated by default; only `qop=auth-int` covers it
  • Believing the server can store a bcrypt hash and still verify a digest

context