You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 13676b7
Browse filesBrowse the repository at this point in the historyBrowse files
- Refresh the held DPoP nonce from every response's DPoP-Nonce header
(RFC 9449 §8.2 newest-wins) instead of capturing it once, so a server that
rotates or single-uses its nonce no longer turns the negative probes into
false "not testable" failures (or vacuous passes). (#1)
- Widen the stale iat probe to -303 s (matching the future side) so the ±1 s
quantization of the whole-second Date header can't pull it onto the ±300 s
boundary and be false-accepted. (#2)
- Update the isNonceChallenge comment and the untestable message: probes now
carry the held nonce, so a use_dpop_nonce challenge is about the nonce
lifetime (rotated/stale/single-use), not a missing nonce. (#3)
- Fixture: parse integer env vars NaN-safely (intEnv helper) so a malformed
DPOP_CLOCK_OFFSET_SECONDS / DPOP_IAT_SKEW_SECONDS falls back to its default
instead of silently disabling the iat window. (#4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
// A `use_dpop_nonce` challenge means the server is demanding a nonce before it
104
-
// will look at the proof. The negative probes are sent without a nonce, so such
105
-
// a 401 is a rejection for the MISSING NONCE, not for the injected defect — it
106
-
// cannot be attributed to the defect and must not count as a proper rejection.
103
+
// A `use_dpop_nonce` challenge means the server is demanding a (different) nonce
104
+
// before it will look at the proof. Negative probes carry the held nonce, so
105
+
// such a 401 means the server did not accept that nonce (it rotated or
106
+
// single-uses it, or wants a fresh one) — the rejection is about the nonce
107
+
// lifetime, NOT the injected defect, so it cannot be attributed to the defect
108
+
// and must not count as a proper rejection.
107
109
functionisNonceChallenge(res: Response): boolean{
108
110
return(
109
111
res.statusCode===401&&
@@ -192,7 +194,7 @@ function rejectionCheck(
192
194
id,
193
195
name,
194
196
description,
195
-
`server answered with a DPoP nonce challenge (use_dpop_nonce), so this rejection cannot be attributed to the ${String(details.case??'injected')} defect rather than the missing nonce`,
197
+
`server answered with a DPoP nonce challenge (use_dpop_nonce) despite the probe carrying the held nonce, so this rejection is about the nonce (rotated/stale/single-use) and cannot be attributed to the ${String(details.case??'injected')} defect`,
196
198
SPEC_REFERENCES
197
199
);
198
200
}
@@ -258,6 +260,15 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
258
260
jkt: kp.thumbprint
259
261
});
260
262
263
+
// The server-provided nonce (RFC 9449 §8/§9), if the server requires one.
264
+
// `send` refreshes it from every response's DPoP-Nonce header (newest wins,
265
+
// RFC 9449 §8.2), and the local `buildDpopProof` wrapper folds the current
266
+
// value into every subsequent proof — including the negatives — so a server
267
+
// that checks the nonce first still evaluates the injected defect rather
268
+
// than merely re-challenging. Refreshing (vs capturing once) keeps this
269
+
// correct against servers that rotate or single-use their nonces.
270
+
letheldNonce: string|undefined;
271
+
261
272
constsend=async(
262
273
authz: string,
263
274
dpop: string|string[]|undefined
@@ -296,15 +307,11 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
296
307
constdate=Number.isNaN(parsedDate)
297
308
? undefined
298
309
: Math.floor(parsedDate/1000);
310
+
// Newest-wins (RFC 9449 §8.2): carry the latest nonce into the next probe.
311
+
if(dpopNonce)heldNonce=dpopNonce;
299
312
return{statusCode: res.statusCode, wwwAuthenticate, dpopNonce, date };
300
313
};
301
314
302
-
// The server-provided nonce captured from the baseline handshake (RFC 9449
303
-
// §9), if the server requires one. Once set, the local `buildDpopProof`
304
-
// wrapper folds it into every subsequent proof — including the negatives —
305
-
// so a server that checks the nonce first still evaluates the injected
306
-
// defect rather than merely challenging for a nonce.
307
-
letheldNonce: string|undefined;
308
315
constbuildDpopProof=(
309
316
opts: Parameters<typeofbaseBuildDpopProof>[0]
310
317
): Promise<string>=>
@@ -333,7 +340,8 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
333
340
first.dpopNonce&&
334
341
first.wwwAuthenticate.includes('use_dpop_nonce')
335
342
){
336
-
heldNonce=first.dpopNonce;
343
+
// `send` has already refreshed heldNonce from the challenge response,
344
+
// so the retry's proof carries it.
337
345
returnsend(`DPoP ${token}`,awaitvalidProof());
338
346
}
339
347
returnfirst;
@@ -637,12 +645,15 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
637
645
}
638
646
639
647
// ---- iat acceptance window ----
640
-
// Probes sit just outside the ±5-minute window (±300 s). The future probe
641
-
// uses +303 s (not +301) because `iat` is whole-seconds and the proof drifts
642
-
// ~1 s toward "now" in transit; +301 could read as +300 (in-window) at the
643
-
// server and false-accept. The stale side only ages further, so −301 is safe.
648
+
// Probes sit just outside the ±5-minute window (±300 s), ±303 s on both
649
+
// sides. The 3 s margin absorbs two sources of ±1 s error that a bare ±301
650
+
// would not: the whole-second `Date` header makes `serverClockOffset`
651
+
// quantized to ±1 s, and `iat` is whole-seconds and drifts ~1 s toward "now"
652
+
// in transit. At ±301 either could pull the probe onto the ±300 boundary and
653
+
// be false-accepted; ±303 stays safely outside for a conformant server while
654
+
// still being well inside a rejection for any sane implementation.
0 commit comments