@absolutejs/auth 0.75.0 → 0.75.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -281,8 +281,11 @@ const authPlugin = await auth({
281
281
  resource: 'https://api.example.com',
282
282
  scopes: ['documents:read', 'documents:write'],
283
283
  verifyCredential: createOidcAgentCredentialVerifier({
284
+ // Atomically insert a hash of jkt + jti; return false on conflict.
285
+ consumeDpopJti: replayStore.consume,
284
286
  issuer: 'https://auth.example.com',
285
287
  publicJwk: signingKey.publicJwk,
288
+ requireDpop: true,
286
289
  resource: 'https://api.example.com'
287
290
  })
288
291
  },
@@ -300,6 +303,16 @@ registration. Approval through the existing RFC 8628 device flow creates the
300
303
  user-to-agent delegation. The agent can then use RFC 8693 token exchange to get
301
304
  a narrowed, audience-bound access token for the protected API.
302
305
 
306
+ When `requireDpop` is enabled, the adapter accepts only an RFC 9449-bound
307
+ access token using the `DPoP` authorization scheme, verifies its per-request
308
+ proof and `ath` token hash, and requires the proof key to match `cnf.jkt`.
309
+ Provide `consumeDpopJti` as an atomic shared-store insertion in clustered
310
+ deployments; returning `false` rejects a replay. Proofs without `jti`, proofs
311
+ whose JWK contains private key material, oversized identifiers, and `htu`
312
+ claims containing query or fragment components fail closed. Resource servers
313
+ that require RFC 9449 nonces can use the nonce helpers exported by
314
+ `@absolutejs/auth/oidc` to issue a separate resource nonce challenge.
315
+
303
316
  ```ts
304
317
  app.get('/documents', ({ protectAgent }) =>
305
318
  protectAgent(['documents:read'], (agent) => ({
@@ -1349,6 +1349,7 @@ var validateClientIdMetadataDocument = (document, expectedClientId) => {
1349
1349
  // src/oidc/dpop.ts
1350
1350
  init_constants();
1351
1351
  var DEFAULT_MAX_AGE_MS = 60000;
1352
+ var MAX_JTI_LENGTH = 128;
1352
1353
  var SECONDS_TO_MS = 1000;
1353
1354
  var NONCE_WINDOW_SECONDS = 120;
1354
1355
  var NONCE_WINDOW_MS = NONCE_WINDOW_SECONDS * MILLISECONDS_IN_A_SECOND;
@@ -1389,17 +1390,38 @@ var verifyDpopNonce = async ({
1389
1390
  const candidates = await Promise.all(Array.from({ length: NONCE_PREVIOUS_WINDOWS_ACCEPTED + 1 }, (_, offset) => hmacSha256(secret, String(currentWindow - offset))));
1390
1391
  return candidates.some((expected) => expected === nonce);
1391
1392
  };
1392
- var decodeHeader = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
1393
- var normalizeHtu = (value) => {
1393
+ var decodeHeader = (segment) => {
1394
+ try {
1395
+ const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
1396
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
1397
+ } catch {
1398
+ return;
1399
+ }
1400
+ };
1401
+ var normalizeHtu = (value, proofClaim = false) => {
1394
1402
  try {
1395
1403
  const url = new URL(String(value));
1404
+ if (url.username || url.password || proofClaim && (url.search.length > 0 || url.hash.length > 0))
1405
+ return;
1396
1406
  return `${url.origin}${url.pathname}`;
1397
1407
  } catch {
1398
- return "";
1408
+ return;
1399
1409
  }
1400
1410
  };
1411
+ var isPublicEs256Jwk = (value) => {
1412
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1413
+ return false;
1414
+ const kty = Reflect.get(value, "kty");
1415
+ const crv = Reflect.get(value, "crv");
1416
+ const xCoordinate = Reflect.get(value, "x");
1417
+ const yCoordinate = Reflect.get(value, "y");
1418
+ const privateComponent = Reflect.get(value, "d");
1419
+ const alg = Reflect.get(value, "alg");
1420
+ return kty === "EC" && crv === "P-256" && typeof xCoordinate === "string" && xCoordinate.length > 0 && typeof yCoordinate === "string" && yCoordinate.length > 0 && privateComponent === undefined && (alg === undefined || alg === "ES256");
1421
+ };
1401
1422
  var verifyDpopProof = async ({
1402
1423
  accessToken,
1424
+ consumeJti,
1403
1425
  htm,
1404
1426
  htu,
1405
1427
  isUsedJti,
@@ -1409,14 +1431,20 @@ var verifyDpopProof = async ({
1409
1431
  }) => {
1410
1432
  if (proof === undefined)
1411
1433
  return;
1412
- const [headerSegment] = proof.split(".");
1434
+ const segments = proof.split(".");
1435
+ if (segments.length !== 3)
1436
+ return;
1437
+ const [headerSegment] = segments;
1413
1438
  if (headerSegment === undefined)
1414
1439
  return;
1415
1440
  const header = decodeHeader(headerSegment);
1416
- if (header?.typ !== "dpop+jwt" || header.alg !== "ES256" || header.jwk === undefined) {
1441
+ const publicJwk = header === undefined ? undefined : Reflect.get(header, "jwk");
1442
+ if (header === undefined || Reflect.get(header, "typ") !== "dpop+jwt" || Reflect.get(header, "alg") !== "ES256" || !isPublicEs256Jwk(publicJwk)) {
1417
1443
  return;
1418
1444
  }
1419
- const verified = await verifyJwt(proof, header.jwk);
1445
+ const verified = await verifyJwt(proof, publicJwk).catch(() => {
1446
+ return;
1447
+ });
1420
1448
  if (verified === undefined)
1421
1449
  return;
1422
1450
  const { payload } = verified;
@@ -1426,14 +1454,19 @@ var verifyDpopProof = async ({
1426
1454
  return;
1427
1455
  }
1428
1456
  const iatMs = typeof payload.iat === "number" ? payload.iat * SECONDS_TO_MS : 0;
1429
- if (payload.htm !== htm || normalizeHtu(payload.htu) !== normalizeHtu(htu) || iatMs === 0 || Math.abs(now - iatMs) > maxAgeMs) {
1457
+ const claimedHtu = normalizeHtu(payload.htu, true);
1458
+ const expectedHtu = normalizeHtu(htu);
1459
+ const jti = typeof payload.jti === "string" ? payload.jti : undefined;
1460
+ if (payload.htm !== htm || claimedHtu === undefined || expectedHtu === undefined || claimedHtu !== expectedHtu || iatMs === 0 || Math.abs(now - iatMs) > maxAgeMs || jti === undefined || jti.length === 0 || jti.length > MAX_JTI_LENGTH) {
1430
1461
  return;
1431
1462
  }
1432
- const jti = typeof payload.jti === "string" ? payload.jti : undefined;
1433
- if (jti !== undefined && isUsedJti !== undefined && await isUsedJti(jti)) {
1463
+ const jkt = await jwkThumbprint(publicJwk);
1464
+ if (isUsedJti !== undefined && await isUsedJti(jti)) {
1434
1465
  return;
1435
1466
  }
1436
- return { jkt: await jwkThumbprint(header.jwk), jti };
1467
+ if (consumeJti !== undefined && !await consumeJti({ expiresAt: iatMs + maxAgeMs, jkt, jti }))
1468
+ return;
1469
+ return { jkt, jti };
1437
1470
  };
1438
1471
 
1439
1472
  // src/agents/oidcAdapter.ts
@@ -1449,6 +1482,7 @@ var readAudience = (audience) => {
1449
1482
  };
1450
1483
  var createOidcAgentCredentialVerifier = ({
1451
1484
  issuer,
1485
+ consumeDpopJti,
1452
1486
  isUsedDpopJti,
1453
1487
  maxDpopAgeMs,
1454
1488
  publicJwk,
@@ -1477,6 +1511,7 @@ var createOidcAgentCredentialVerifier = ({
1477
1511
  return;
1478
1512
  const proof = await verifyDpopProof({
1479
1513
  accessToken: token,
1514
+ consumeJti: consumeDpopJti,
1480
1515
  htm: request.method,
1481
1516
  htu: request.url,
1482
1517
  isUsedJti: isUsedDpopJti,
@@ -13763,5 +13798,5 @@ export {
13763
13798
  AGENT_CLAIM_GRANT_TYPE
13764
13799
  };
13765
13800
 
13766
- //# debugId=323598F91D83FA7764756E2164756E21
13801
+ //# debugId=801AFAC38E904B9A64756E2164756E21
13767
13802
  //# sourceMappingURL=index.js.map