Part of the audit remediation umbrella #67. Milestone 3. Severity: HIGH. Requirements: AUTH-12, AUTH-16, AUTH-17, AUTH-22, AUTH-25.
Current SDK behavior
Only the first challenge header is read. pickChallengeHeader
(packages/core/src/auth/auth-step.ts:420,423) calls response.headers.get(...), which
returns the first value. @dexpace/transport-undici delivers a repeated WWW-Authenticate
as an array, and undici-transport.ts:334-335 pushes each element as its own entry. Every
challenge after the first is discarded. A 401 whose first line is
Digest … algorithm=SHA-512-256 (unsupported) and whose second is
Digest … algorithm=SHA-256, qop="auth" ends with one send, status 401, no Authorization.
The same two challenges in one comma-joined value end with two sends and status 200.
RFC 7616 §3.3 recommends one header per algorithm. @dexpace/transport-fetch comma-joins, so
it is unaffected. The comment at undici-transport.ts:334 says arrays are for Set-Cookie
only; undici arrays any repeated header.
-sess without qop sends an unverifiable response. A challenge with
algorithm=MD5-sess (or SHA-256-sess) and no qop is accepted (digest.ts:250-258). HA1 is
H(H(u:r:p):nonce:cnonce) with a fresh random cnonce (:337-343), but cnonce is written to
the header only when qop=auth is present (:386-387). The server cannot recompute HA1.
Every such request fails, and AUTH-30 bounds the replay to one 401.
Empty nonce / realm count as present. digest.ts:246-248 tests === undefined. A
truncated WWW-Authenticate: Digest realm="r", nonce= yields nonce: '', is accepted, and the
client sends nonce="".
Expected behavior
The auth step sees every challenge the server sent, in order, whichever transport delivered
the response. ChallengeHandler.rank selects across all of them. A Digest challenge the SDK
cannot answer verifiably is declined and the next challenge is tried. A challenge with an
empty realm or nonce is not satisfiable.
Notes and leads
pickChallengeHeader: use headers.getAll(...) and either join with , before parsing or
parse each value and concatenate the challenge lists. RFC 9110 §11.6.1 permits both wire
shapes. Same for Proxy-Authenticate.
-sess without qop: RFC 7616 §3.4.2 requires cnonce for -sess regardless of qop.
Emitting cnonce is what servers expect. AUTH-22 as written forbids it. Decide, then record
the reading in the Phase 5c ledger section. The alternative, declining the challenge, needs no
ledger row.
- Empty values: require non-empty strings for
realm and nonce.
- Tests: core test with two
WWW-Authenticate entries; a conformance row in
packages/transport-conformance asserting both transports surface repeated challenge headers
identically; computeDigestResponse vector for MD5-sess with no qop; empty nonce.
- Fix the undici comment. Patch changeset for
@dexpace/core.
Part of the audit remediation umbrella #67. Milestone 3. Severity: HIGH. Requirements: AUTH-12, AUTH-16, AUTH-17, AUTH-22, AUTH-25.
Current SDK behavior
Only the first challenge header is read.
pickChallengeHeader(
packages/core/src/auth/auth-step.ts:420,423) callsresponse.headers.get(...), whichreturns the first value.
@dexpace/transport-undicidelivers a repeatedWWW-Authenticateas an array, and
undici-transport.ts:334-335pushes each element as its own entry. Everychallenge after the first is discarded. A 401 whose first line is
Digest … algorithm=SHA-512-256(unsupported) and whose second isDigest … algorithm=SHA-256, qop="auth"ends with one send, status 401, noAuthorization.The same two challenges in one comma-joined value end with two sends and status 200.
RFC 7616 §3.3 recommends one header per algorithm.
@dexpace/transport-fetchcomma-joins, soit is unaffected. The comment at
undici-transport.ts:334says arrays are forSet-Cookieonly; undici arrays any repeated header.
-sesswithoutqopsends an unverifiable response. A challenge withalgorithm=MD5-sess(orSHA-256-sess) and noqopis accepted (digest.ts:250-258). HA1 isH(H(u:r:p):nonce:cnonce)with a fresh random cnonce (:337-343), butcnonceis written tothe header only when
qop=authis present (:386-387). The server cannot recompute HA1.Every such request fails, and AUTH-30 bounds the replay to one 401.
Empty
nonce/realmcount as present.digest.ts:246-248tests=== undefined. Atruncated
WWW-Authenticate: Digest realm="r", nonce=yieldsnonce: '', is accepted, and theclient sends
nonce="".Expected behavior
The auth step sees every challenge the server sent, in order, whichever transport delivered
the response.
ChallengeHandler.rankselects across all of them. A Digest challenge the SDKcannot answer verifiably is declined and the next challenge is tried. A challenge with an
empty
realmornonceis not satisfiable.Notes and leads
pickChallengeHeader: useheaders.getAll(...)and either join with,before parsing orparse each value and concatenate the challenge lists. RFC 9110 §11.6.1 permits both wire
shapes. Same for
Proxy-Authenticate.-sesswithoutqop: RFC 7616 §3.4.2 requirescnoncefor-sessregardless ofqop.Emitting
cnonceis what servers expect. AUTH-22 as written forbids it. Decide, then recordthe reading in the Phase 5c ledger section. The alternative, declining the challenge, needs no
ledger row.
realmandnonce.WWW-Authenticateentries; a conformance row inpackages/transport-conformanceasserting both transports surface repeated challenge headersidentically;
computeDigestResponsevector forMD5-sesswith noqop; emptynonce.@dexpace/core.