@absolutejs/auth 0.74.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.
Files changed (47) hide show
  1. package/README.md +21 -6
  2. package/dist/agents/index.js +46 -11
  3. package/dist/agents/index.js.map +4 -4
  4. package/dist/agents/oidcAdapter.d.ts +3 -1
  5. package/dist/apikeys/routes.d.ts +1 -1
  6. package/dist/compliance/routes.d.ts +1 -1
  7. package/dist/credentials/emailVerification.d.ts +1 -1
  8. package/dist/credentials/login.d.ts +1 -1
  9. package/dist/credentials/passwordReset.d.ts +1 -1
  10. package/dist/credentials/register.d.ts +1 -1
  11. package/dist/htmx/configuredRoutes.d.ts +1 -1
  12. package/dist/htmx/routes.d.ts +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +166 -68
  15. package/dist/index.js.map +8 -8
  16. package/dist/mfa/challenge.d.ts +1 -1
  17. package/dist/mfa/management.d.ts +1 -1
  18. package/dist/mfa/sms.d.ts +1 -1
  19. package/dist/mfa/totp.d.ts +1 -1
  20. package/dist/nativePush.d.ts +500 -20
  21. package/dist/oidc/dpop.d.ts +6 -2
  22. package/dist/oidc/index.d.ts +1 -0
  23. package/dist/oidc/index.js +130 -1
  24. package/dist/oidc/index.js.map +4 -3
  25. package/dist/organizations/routes.d.ts +1 -1
  26. package/dist/portal/routes.d.ts +1 -1
  27. package/dist/roles/routes.d.ts +1 -1
  28. package/dist/routes/authorize.d.ts +1 -1
  29. package/dist/routes/callback.d.ts +1 -1
  30. package/dist/routes/profile.d.ts +1 -1
  31. package/dist/routes/refresh.d.ts +1 -1
  32. package/dist/routes/revoke.d.ts +1 -1
  33. package/dist/routes/sessions.d.ts +1 -1
  34. package/dist/routes/signout.d.ts +1 -1
  35. package/dist/routes/userStatus.d.ts +1 -1
  36. package/dist/scim/routes.d.ts +1 -1
  37. package/dist/server.js +164 -68
  38. package/dist/server.js.map +8 -8
  39. package/dist/sso/discoveryRoute.d.ts +1 -1
  40. package/dist/sso/oidcRoutes.d.ts +1 -1
  41. package/dist/sso/samlIdpRoutes.d.ts +1 -1
  42. package/dist/sso/samlRoutes.d.ts +1 -1
  43. package/dist/types.d.ts +3 -1
  44. package/dist/vc/statusListRoutes.d.ts +1 -1
  45. package/dist/vc/vpRoutes.d.ts +1 -1
  46. package/dist/webauthn/routes.d.ts +1 -1
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -118,13 +118,14 @@ stacked and reversible, and web/server runtimes never install the registry.
118
118
  When portable push is enabled, Auth also owns its authenticated installation
119
119
  boundary. Pass the existing Dispatch push lifecycle directly; the server derives
120
120
  the principal, tenant, and authorized topics and returns an opaque installation
121
- identity. The native shell is the only code that sees the APNs/FCM token, and
122
- ordinary application/page code cannot read it:
121
+ identity. The native shell handles APNs/FCM tokens, while the generated PWA
122
+ runtime handles structured browser subscriptions; ordinary page code reads
123
+ neither provider credential:
123
124
 
124
125
  ```ts
125
126
  const authApplication = await auth({
126
127
  // ...normal Auth + OIDC configuration
127
- nativePush: {
128
+ push: {
128
129
  registrar: pushLifecycle,
129
130
  tenant: (principal) => principal.user.organizationId,
130
131
  topics: (principal) => topicsFor(principal.user)
@@ -132,9 +133,10 @@ const authApplication = await auth({
132
133
  });
133
134
  ```
134
135
 
135
- The fixed `/auth/mobile/push` route accepts bearer-authenticated native clients
136
- and cookie-authenticated web clients, but never accepts user ID, tenant, or
137
- topics from either. Token rotation and deletion require the server-issued
136
+ The fixed `/auth/push` route accepts bearer-authenticated native clients and
137
+ cookie-authenticated web clients, but never accepts user ID, tenant, or topics
138
+ from either. The prior `/auth/mobile/push` path remains an installed-client
139
+ compatibility alias. Credential rotation and deletion require the server-issued
138
140
  installation identity and Dispatch verifies that it belongs to the current
139
141
  principal. Native sign-out attempts removal while credentials still exist and
140
142
  always clears the local provider registration even when the network is down.
@@ -279,8 +281,11 @@ const authPlugin = await auth({
279
281
  resource: 'https://api.example.com',
280
282
  scopes: ['documents:read', 'documents:write'],
281
283
  verifyCredential: createOidcAgentCredentialVerifier({
284
+ // Atomically insert a hash of jkt + jti; return false on conflict.
285
+ consumeDpopJti: replayStore.consume,
282
286
  issuer: 'https://auth.example.com',
283
287
  publicJwk: signingKey.publicJwk,
288
+ requireDpop: true,
284
289
  resource: 'https://api.example.com'
285
290
  })
286
291
  },
@@ -298,6 +303,16 @@ registration. Approval through the existing RFC 8628 device flow creates the
298
303
  user-to-agent delegation. The agent can then use RFC 8693 token exchange to get
299
304
  a narrowed, audience-bound access token for the protected API.
300
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
+
301
316
  ```ts
302
317
  app.get('/documents', ({ protectAgent }) =>
303
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