Skip to content

Commit 248e957

Browse files
server/dpop: emit RejectsBearerScheme as its own check id
SEP-1932 now requires rejecting a DPoP-bound token presented as Bearer, and the traceability row sep-1932-server-reject-bearer-downgrade names that requirement. The result path and the probe-error path both use that id. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ff777cc commit 248e957

2 files changed

Lines changed: 41 additions & 32 deletions

File tree

‎src/scenarios/server/auth/dpop.test.ts‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,16 @@ describe('DPoP server validation scenario', () => {
244244
)
245245
).toBe(true);
246246
expect(
247-
byId(checks, 'sep-1932-server-validate-proof').find(
247+
byId(checks, 'sep-1932-server-validate-proof').some(
248248
(c) => c.name === 'RejectsBearerScheme'
249-
)?.status
250-
).toBe('SUCCESS');
249+
)
250+
).toBe(false);
251+
expect(
252+
byId(checks, 'sep-1932-server-reject-bearer-downgrade').map((c) => [
253+
c.name,
254+
c.status
255+
])
256+
).toEqual([['RejectsBearerScheme', 'SUCCESS']]);
251257
expect(
252258
byId(checks, 'sep-1932-server-iat-window').every(
253259
(c) => c.status === 'SUCCESS'
@@ -282,6 +288,12 @@ describe('DPoP server validation scenario', () => {
282288
);
283289
expect(rejects.length).toBeGreaterThan(0);
284290
expect(rejects.every((c) => c.status === 'FAILURE')).toBe(true);
291+
expect(
292+
byId(checks, 'sep-1932-server-reject-bearer-downgrade').map((c) => [
293+
c.name,
294+
c.status
295+
])
296+
).toEqual([['RejectsBearerScheme', 'FAILURE']]);
285297

286298
expect(byId(checks, 'sep-1932-server-reject-401')[0].status).toBe(
287299
'FAILURE'
@@ -319,6 +331,7 @@ describe('DPoP server validation scenario', () => {
319331
...byId(checks, 'sep-1932-server-validate-proof').filter((c) =>
320332
c.name.startsWith('Rejects')
321333
),
334+
...byId(checks, 'sep-1932-server-reject-bearer-downgrade'),
322335
...byId(checks, 'sep-1932-server-iat-window'),
323336
...byId(checks, 'sep-1932-asymmetric-alg-only'),
324337
...byId(checks, 'sep-1932-server-audience-validation'),
@@ -365,6 +378,7 @@ describe('DPoP server validation scenario', () => {
365378
...byId(checks, 'sep-1932-server-validate-proof').filter((c) =>
366379
c.name.startsWith('Rejects')
367380
),
381+
...byId(checks, 'sep-1932-server-reject-bearer-downgrade'),
368382
...byId(checks, 'sep-1932-server-iat-window'),
369383
...byId(checks, 'sep-1932-asymmetric-alg-only'),
370384
...byId(checks, 'sep-1932-server-audience-validation'),
@@ -402,16 +416,14 @@ describe('DPoP server validation scenario', () => {
402416
testContext(url(ports.bearerReject))
403417
);
404418

405-
const bearer = byId(checks, 'sep-1932-server-validate-proof').find(
406-
(c) => c.name === 'RejectsBearerScheme'
407-
);
408-
expect(bearer?.status).toBe('FAILURE');
419+
const bearer = byId(checks, 'sep-1932-server-reject-bearer-downgrade');
420+
expect(bearer.map((c) => [c.name, c.status])).toEqual([
421+
['RejectsBearerScheme', 'FAILURE']
422+
]);
409423

410-
// This fixture only mishandles Bearer-scheme presentation — every other
424+
// This fixture only mishandles Bearer-scheme presentation — every
411425
// validate-proof probe still behaves as on the compliant server.
412-
const others = byId(checks, 'sep-1932-server-validate-proof').filter(
413-
(c) => c.name !== 'RejectsBearerScheme'
414-
);
426+
const others = byId(checks, 'sep-1932-server-validate-proof');
415427
expect(others.length).toBeGreaterThan(0);
416428
expect(others.every((c) => c.status === 'SUCCESS')).toBe(true);
417429
}, 30000);
@@ -421,19 +433,17 @@ describe('DPoP server validation scenario', () => {
421433
testContext(url(ports.bearerChallenge))
422434
);
423435

424-
const bearer = byId(checks, 'sep-1932-server-validate-proof').find(
425-
(c) => c.name === 'RejectsBearerScheme'
426-
);
427-
expect(bearer?.status).toBe('SUCCESS');
436+
const bearer = byId(checks, 'sep-1932-server-reject-bearer-downgrade');
437+
expect(bearer.map((c) => [c.name, c.status])).toEqual([
438+
['RejectsBearerScheme', 'SUCCESS']
439+
]);
428440
// The compliant server challenges with DPoP. This fixture must actually
429441
// send a Bearer challenge, or SUCCESS would only re-prove the DPoP arm.
430-
expect(bearer?.details?.wwwAuthenticate).toBe(
442+
expect(bearer[0]?.details?.wwwAuthenticate).toBe(
431443
'Bearer error="invalid_token"'
432444
);
433445

434-
const others = byId(checks, 'sep-1932-server-validate-proof').filter(
435-
(c) => c.name !== 'RejectsBearerScheme'
436-
);
446+
const others = byId(checks, 'sep-1932-server-validate-proof');
437447
expect(others.length).toBeGreaterThan(0);
438448
expect(others.every((c) => c.status === 'SUCCESS')).toBe(true);
439449
}, 30000);

‎src/scenarios/server/auth/dpop.ts‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,10 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
414414
buildDpop: () => Promise<string | string[]>;
415415
predicate?: (res: Response) => boolean;
416416
description?: string;
417+
// Defaults to sep-1932-server-validate-proof. A case whose requirement
418+
// is a separate yaml row sets its own id; the catch path uses the same
419+
// id so a probe that throws is still attributed to that requirement.
420+
id?: string;
417421
}> = [
418422
{
419423
case: 'tampered-signature',
@@ -505,12 +509,13 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
505509
})
506510
},
507511
{
508-
// A DPoP-bound token presented under the Bearer scheme MUST NOT be
509-
// accepted. Count only a real authentication rejection: 401 plus a
510-
// Bearer (RFC 6750 §3) or DPoP challenge. A 500/404 with no
511-
// WWW-Authenticate is not a rejection of the scheme.
512+
// Not an RFC 9449 §4.3 step. SEP-1932 requires rejecting a DPoP-bound
513+
// token presented as Bearer with 401 plus a Bearer (RFC 6750 §3) or
514+
// DPoP challenge. A 500/404 with no WWW-Authenticate is not that
515+
// rejection. Own check id: sep-1932-server-reject-bearer-downgrade.
512516
case: 'bearer-scheme',
513517
name: 'RejectsBearerScheme',
518+
id: 'sep-1932-server-reject-bearer-downgrade',
514519
authz: `Bearer ${token}`,
515520
buildDpop: () => validProof(),
516521
predicate: bearerSchemeRejected,
@@ -595,13 +600,14 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
595600
for (const n of negatives) {
596601
const description =
597602
n.description ?? `Server rejects a DPoP request with defect: ${n.case}`;
603+
const id = n.id ?? 'sep-1932-server-validate-proof';
598604
try {
599605
const dpop = await n.buildDpop();
600606
const res = await send(n.authz, dpop);
601607
checks.push(
602608
rejectionCheck(
603609
positiveAccepted,
604-
'sep-1932-server-validate-proof',
610+
id,
605611
n.name,
606612
description,
607613
res,
@@ -611,14 +617,7 @@ algorithms, the 401 challenge format, token audience validation under DPoP, and
611617
);
612618
} catch (e) {
613619
checks.push(
614-
probeErrorCheck(
615-
positiveAccepted,
616-
'sep-1932-server-validate-proof',
617-
n.name,
618-
description,
619-
n.case,
620-
e
621-
)
620+
probeErrorCheck(positiveAccepted, id, n.name, description, n.case, e)
622621
);
623622
}
624623
}

0 commit comments

Comments
 (0)