How to test digest auth with curl

Run curl -v --digest --user user:passwd https://host/private. The verbose transcript holds two requests. The first carries no credentials and earns a real 401 with a nonce; the second answers it with an MD5 response= value and earns the 200. Without --digest curl sends Basic and the server rejects it.

Why check this

Digest auth turns up on appliances, printers, camera firmware, older Java and IIS applications, and internal tools nobody has migrated. Run this check when a client cannot reach such an endpoint, when a proxy or gateway is put in front of one, and on any regression pass that touches the authentication layer.

The failure it prevents is specific. A load balancer that spreads requests across nodes breaks digest, because the node that issued the nonce is not the node that has to verify it. The symptom is intermittent 401 responses that nobody can reproduce, and the transcript below is how you show that the client answered the challenge correctly and a different node refused it. The reason is visible in step 1: the exchange is stateful across two requests, and basic auth is not.

The same transcript answers the question testers actually ask about digest. The password itself never travels. What travels is a hash over the password, the realm, the nonce, the method and the URI, so a wrong password and a correct one produce requests of the same shape.

Prerequisites

Steps

  1. Step 1.

    Run the exchange with --digest and keep the whole transcript.

    curl -s -v --digest --user user:passwd https://httpbin.org/digest-auth/auth/user/passwd
    
    > GET /digest-auth/auth/user/passwd HTTP/2
    > Host: httpbin.org
    > User-Agent: curl/8.21.0
    > Accept: */*
    >
    < HTTP/2 401
    < content-type: text/html; charset=utf-8
    < content-length: 0
    < www-authenticate: Digest realm="me@kennethreitz.com", nonce="cff74cbce4a8ffa33fcf624e4b0c4fe7", qop="auth", opaque="fb205ea3b186b62faca3e54852ab6eb1", algorithm=MD5, stale=FALSE
    < set-cookie: stale_after=never; Path=/
    <
    > GET /digest-auth/auth/user/passwd HTTP/2
    > Host: httpbin.org
    > Authorization: Digest username="user",realm="me@kennethreitz.com",nonce="cff74cbce4a8ffa33fcf624e4b0c4fe7",uri="/digest-auth/auth/user/passwd",cnonce="44a0091eb9bf79e69344085ed1e45319",nc=00000001,algorithm=MD5,response="ffdada36758f81ffb299416501fb6fb3",qop="auth",opaque="fb205ea3b186b62faca3e54852ab6eb1"
    > User-Agent: curl/8.21.0
    > Accept: */*
    >
    < HTTP/2 200
    < content-type: application/json
    < content-length: 47
    <
    {
    "authenticated": true,
    "user": "user"
    }

    Read the two nonce values. The client echoed the one the server issued, unchanged.

  2. Step 2.

    Drop --digest and send the same correct password.

    curl -s -v --user user:passwd https://httpbin.org/digest-auth/auth/user/passwd
    
    > Authorization: Basic dXNlcjpwYXNzd2Q=
    …
    < HTTP/2 401
    < www-authenticate: Digest realm="me@kennethreitz.com", nonce="6a242b9e5971cec3b487b64cb76d78b3", qop="auth", opaque="1c115c2b54e865de28d588cc67910842", algorithm=MD5, stale=FALSE

    One request, a correct password, and a 401. curl offered Basic, the server wants Digest, and neither side says so in plain words.

  3. Step 3.

    Run the exchange again with a wrong password and compare it with step 1.

    curl -s -v --digest --user user:wrong https://httpbin.org/digest-auth/auth/user/passwd
    
    > Authorization: Digest username="user",realm="me@kennethreitz.com",nonce="5cfefba19684c0233ac7f7078fbc848c",uri="/digest-auth/auth/user/passwd",cnonce="0a356633cbcd3bafc00cbd221a92ad76",nc=00000001,algorithm=MD5,response="da7dc474906c143b802d46932a907058",qop="auth",opaque="f7cb4015b2408332f1b26003764a870a"
    …
    < HTTP/2 401
    < www-authenticate: Digest realm="me@kennethreitz.com", nonce="38a5f3a7c594886e29e4bac72a465216", qop="auth", opaque="41671cfb500f39fd72b277803e497cb4", algorithm=MD5, stale=FALSE
    < set-cookie: last_nonce=5cfefba19684c0233ac7f7078fbc848c; Path=/

    The header has the same fields as the accepted one in step 1. Only response= differs, and the string wrong appears nowhere.

  4. Step 4.

    Let curl choose the scheme when you do not know it.

    curl -s -v --anyauth --user user:passwd https://httpbin.org/digest-auth/auth/user/passwd
    
    > GET /digest-auth/auth/user/passwd HTTP/2
    > User-Agent: curl/8.21.0
    >
    < HTTP/2 401
    < www-authenticate: Digest realm="me@kennethreitz.com", nonce="3a869a0cf647dd18529182f15dc20590", qop="auth", opaque="ce76ebc2b5a547458931293f3e6f6fb6", algorithm=MD5, stale=FALSE
    …
    > Authorization: Digest username="user",realm="me@kennethreitz.com",nonce="3a869a0cf647dd18529182f15dc20590",…,response="35fe26cbc670d7c8331b7bfca92b6b7e",qop="auth",opaque="ce76ebc2b5a547458931293f3e6f6fb6"
    …
    < HTTP/2 200

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A 401, then a 200 on the repeat | The exchange worked as designed | Nothing. Two requests per call is normal for digest. | | stale=FALSE on the second 401 | The nonce was accepted, the credentials were not | Fix the password. Retrying will not help. | | stale=TRUE on the second 401 | The nonce expired between the two requests | Retry. A client that does not retry here loses requests under load. | | Authorization: Basic … against a Digest challenge | --digest is missing | Add --digest, or --anyauth when the scheme is unknown. | | A 401 on every second request, passing on the rest | The nonce is issued by one node and checked by another | Pin the session to a node or move the nonce store into shared state. | | algorithm=MD5 with no SHA-256 alternative offered | The server implements RFC 2617 only | Record it. MD5 here is a key derivation, not a transport, so TLS still carries the check. |

Common mistakes

Sign: The first request of every digest call shows up in the access log as a 401, and an alerting rule counts it as a failed login.Cause: The 401 is part of the protocol, not a rejection. A digest endpoint produces one 401 per successful call, so a threshold built on 401 counts fires on normal traffic.
Sign: A correct password is refused and the transcript shows a Basic header.Cause: curl sends Basic unless told otherwise, and it does not fall back when the challenge names another scheme. Step 2 is that failure with a working password.
Sign: Digest works against one server and fails behind the load balancer.Cause: The nonce is state held by the node that issued it. Round-robin sends the second request elsewhere, and that node has never seen the nonce. Nothing in the client transcript looks wrong.
Sign: A test that replays a captured Authorization header passes once and then fails.Cause: The response hash covers the method and the URI, and the server tracks nc per nonce. A replayed header is valid for exactly the request it was made for, so recorded digest traffic cannot be reused in a regression suite.

What to check next

FAQ

Basic vs digest authentication, which should a service use?

Neither, for a new service. Basic hands the password to anyone who reads the traffic, so it needs TLS. Digest hashes the password with MD5 and needs TLS anyway, and its nonce state breaks across nodes. Token schemes avoid both problems.

How does digest auth work in one paragraph?

The server answers an unauthenticated request with 401 and a random nonce. The client hashes the password with the user name, the realm, that nonce, its own cnonce, a counter, the method and the URI, and repeats the request carrying only the hash. The server recomputes it.

Why does curl send two requests for one call?

curl has to see the challenge before it can compute the response, because the nonce comes from the server. That is why --digest produces a 401 in the transcript even on a call that succeeds, as step 1 shows.

Can I skip the first request?

Not reliably. The nonce comes from the server, so a client that has never been challenged has nothing to hash against. A client that caches a nonce from an earlier call can reuse it while the server still accepts it, and a test built on that is testing the server's leniency.

What does opaque do?

The server sends it in the challenge, and the client copies it back unchanged. Step 1 and step 4 show the same value in both directions. It carries server state, so a mismatch usually means the answer reached a different node.

Verified

Verified by Maks Vernycurl 8.21.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate6 minpublished updated Maks Verny