@absolutejs/auth 0.75.0 → 0.75.2

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) => ({
@@ -25,6 +25,9 @@ export type AgentAuthConfig = {
25
25
  oauthGuide?: AgentOAuthGuideConfig;
26
26
  authorizationServer: string;
27
27
  delegationStore: AgentDelegationStore;
28
+ /** Advertise RFC 9728 `dpop_bound_access_tokens_required`. Keep this aligned
29
+ * with a credential verifier configured with `requireDpop: true`. */
30
+ dpopBoundAccessTokensRequired?: boolean;
28
31
  logoUri?: string;
29
32
  metadataRoute?: RouteString;
30
33
  /** Authorization-server route prefix. Defaults to `/oauth2`; used to derive
@@ -39,11 +42,12 @@ export type AgentAuthConfig = {
39
42
  scopes: string[];
40
43
  verifyCredential: AgentCredentialVerifier;
41
44
  };
42
- export declare const agentProtectedResourceMetadata: (config: Pick<AgentAuthConfig, "authorizationServer" | "logoUri" | "resource" | "resourceName" | "scopes">) => {
45
+ export declare const agentProtectedResourceMetadata: (config: Pick<AgentAuthConfig, "authorizationServer" | "dpopBoundAccessTokensRequired" | "logoUri" | "resource" | "resourceName" | "scopes">) => {
43
46
  resource: string;
44
47
  scopes_supported: string[];
45
48
  resource_name?: string | undefined;
46
49
  resource_logo_uri?: string | undefined;
50
+ dpop_bound_access_tokens_required?: boolean | undefined;
47
51
  authorization_servers: string[];
48
52
  bearer_methods_supported: string[];
49
53
  };
@@ -186,6 +186,7 @@ var DEFAULT_AGENT_RESOURCE_METADATA_ROUTE = "/.well-known/oauth-protected-resour
186
186
  var agentProtectedResourceMetadata = (config) => ({
187
187
  authorization_servers: [config.authorizationServer],
188
188
  bearer_methods_supported: ["header"],
189
+ ...config.dpopBoundAccessTokensRequired === true ? { dpop_bound_access_tokens_required: true } : {},
189
190
  ...config.logoUri === undefined ? {} : { resource_logo_uri: config.logoUri },
190
191
  ...config.resourceName === undefined ? {} : { resource_name: config.resourceName },
191
192
  resource: config.resource,
@@ -1349,6 +1350,7 @@ var validateClientIdMetadataDocument = (document, expectedClientId) => {
1349
1350
  // src/oidc/dpop.ts
1350
1351
  init_constants();
1351
1352
  var DEFAULT_MAX_AGE_MS = 60000;
1353
+ var MAX_JTI_LENGTH = 128;
1352
1354
  var SECONDS_TO_MS = 1000;
1353
1355
  var NONCE_WINDOW_SECONDS = 120;
1354
1356
  var NONCE_WINDOW_MS = NONCE_WINDOW_SECONDS * MILLISECONDS_IN_A_SECOND;
@@ -1389,17 +1391,38 @@ var verifyDpopNonce = async ({
1389
1391
  const candidates = await Promise.all(Array.from({ length: NONCE_PREVIOUS_WINDOWS_ACCEPTED + 1 }, (_, offset) => hmacSha256(secret, String(currentWindow - offset))));
1390
1392
  return candidates.some((expected) => expected === nonce);
1391
1393
  };
1392
- var decodeHeader = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
1393
- var normalizeHtu = (value) => {
1394
+ var decodeHeader = (segment) => {
1395
+ try {
1396
+ const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
1397
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
1398
+ } catch {
1399
+ return;
1400
+ }
1401
+ };
1402
+ var normalizeHtu = (value, proofClaim = false) => {
1394
1403
  try {
1395
1404
  const url = new URL(String(value));
1405
+ if (url.username || url.password || proofClaim && (url.search.length > 0 || url.hash.length > 0))
1406
+ return;
1396
1407
  return `${url.origin}${url.pathname}`;
1397
1408
  } catch {
1398
- return "";
1409
+ return;
1399
1410
  }
1400
1411
  };
1412
+ var isPublicEs256Jwk = (value) => {
1413
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1414
+ return false;
1415
+ const kty = Reflect.get(value, "kty");
1416
+ const crv = Reflect.get(value, "crv");
1417
+ const xCoordinate = Reflect.get(value, "x");
1418
+ const yCoordinate = Reflect.get(value, "y");
1419
+ const privateComponent = Reflect.get(value, "d");
1420
+ const alg = Reflect.get(value, "alg");
1421
+ return kty === "EC" && crv === "P-256" && typeof xCoordinate === "string" && xCoordinate.length > 0 && typeof yCoordinate === "string" && yCoordinate.length > 0 && privateComponent === undefined && (alg === undefined || alg === "ES256");
1422
+ };
1401
1423
  var verifyDpopProof = async ({
1402
1424
  accessToken,
1425
+ consumeJti,
1403
1426
  htm,
1404
1427
  htu,
1405
1428
  isUsedJti,
@@ -1409,14 +1432,20 @@ var verifyDpopProof = async ({
1409
1432
  }) => {
1410
1433
  if (proof === undefined)
1411
1434
  return;
1412
- const [headerSegment] = proof.split(".");
1435
+ const segments = proof.split(".");
1436
+ if (segments.length !== 3)
1437
+ return;
1438
+ const [headerSegment] = segments;
1413
1439
  if (headerSegment === undefined)
1414
1440
  return;
1415
1441
  const header = decodeHeader(headerSegment);
1416
- if (header?.typ !== "dpop+jwt" || header.alg !== "ES256" || header.jwk === undefined) {
1442
+ const publicJwk = header === undefined ? undefined : Reflect.get(header, "jwk");
1443
+ if (header === undefined || Reflect.get(header, "typ") !== "dpop+jwt" || Reflect.get(header, "alg") !== "ES256" || !isPublicEs256Jwk(publicJwk)) {
1417
1444
  return;
1418
1445
  }
1419
- const verified = await verifyJwt(proof, header.jwk);
1446
+ const verified = await verifyJwt(proof, publicJwk).catch(() => {
1447
+ return;
1448
+ });
1420
1449
  if (verified === undefined)
1421
1450
  return;
1422
1451
  const { payload } = verified;
@@ -1426,14 +1455,19 @@ var verifyDpopProof = async ({
1426
1455
  return;
1427
1456
  }
1428
1457
  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) {
1458
+ const claimedHtu = normalizeHtu(payload.htu, true);
1459
+ const expectedHtu = normalizeHtu(htu);
1460
+ const jti = typeof payload.jti === "string" ? payload.jti : undefined;
1461
+ 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
1462
  return;
1431
1463
  }
1432
- const jti = typeof payload.jti === "string" ? payload.jti : undefined;
1433
- if (jti !== undefined && isUsedJti !== undefined && await isUsedJti(jti)) {
1464
+ const jkt = await jwkThumbprint(publicJwk);
1465
+ if (isUsedJti !== undefined && await isUsedJti(jti)) {
1434
1466
  return;
1435
1467
  }
1436
- return { jkt: await jwkThumbprint(header.jwk), jti };
1468
+ if (consumeJti !== undefined && !await consumeJti({ expiresAt: iatMs + maxAgeMs, jkt, jti }))
1469
+ return;
1470
+ return { jkt, jti };
1437
1471
  };
1438
1472
 
1439
1473
  // src/agents/oidcAdapter.ts
@@ -1449,6 +1483,7 @@ var readAudience = (audience) => {
1449
1483
  };
1450
1484
  var createOidcAgentCredentialVerifier = ({
1451
1485
  issuer,
1486
+ consumeDpopJti,
1452
1487
  isUsedDpopJti,
1453
1488
  maxDpopAgeMs,
1454
1489
  publicJwk,
@@ -1477,6 +1512,7 @@ var createOidcAgentCredentialVerifier = ({
1477
1512
  return;
1478
1513
  const proof = await verifyDpopProof({
1479
1514
  accessToken: token,
1515
+ consumeJti: consumeDpopJti,
1480
1516
  htm: request.method,
1481
1517
  htu: request.url,
1482
1518
  isUsedJti: isUsedDpopJti,
@@ -13763,5 +13799,5 @@ export {
13763
13799
  AGENT_CLAIM_GRANT_TYPE
13764
13800
  };
13765
13801
 
13766
- //# debugId=323598F91D83FA7764756E2164756E21
13802
+ //# debugId=16DE04B83F43B7D264756E2164756E21
13767
13803
  //# sourceMappingURL=index.js.map