@udibo/oauth2 0.4.0 → 0.5.0

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 (161) hide show
  1. package/README.md +3 -3
  2. package/SECURITY.md +0 -4
  3. package/esm/adapters/hono/_common.d.ts +10 -13
  4. package/esm/adapters/hono/_common.js +11 -20
  5. package/esm/adapters/hono/authorization-server.d.ts +16 -10
  6. package/esm/adapters/hono/authorization-server.js +13 -8
  7. package/esm/adapters/hono/bff/auth-request-store.d.ts +4 -0
  8. package/esm/adapters/hono/bff/auth-request-store.js +4 -0
  9. package/esm/adapters/hono/bff/bff.d.ts +29 -18
  10. package/esm/adapters/hono/bff/bff.js +15 -11
  11. package/esm/adapters/hono/bff/proxy.js +7 -55
  12. package/esm/adapters/hono/bff/session-store.d.ts +9 -9
  13. package/esm/adapters/hono/bff/session-store.js +3 -2
  14. package/esm/adapters/hono/bff/testing.d.ts +14 -3
  15. package/esm/adapters/hono/bff/testing.js +14 -3
  16. package/esm/adapters/hono/identity.d.ts +5 -3
  17. package/esm/adapters/hono/identity.js +1 -17
  18. package/esm/adapters/hono/resource-server.d.ts +3 -2
  19. package/esm/adapters/hono/resource-server.js +3 -2
  20. package/esm/client/_http.d.ts +4 -7
  21. package/esm/client/_http.js +4 -37
  22. package/esm/client/base.d.ts +3 -6
  23. package/esm/client/bff-client.d.ts +7 -6
  24. package/esm/client/bff-client.js +7 -6
  25. package/esm/client/browser-storage.d.ts +33 -34
  26. package/esm/client/browser-storage.js +47 -32
  27. package/esm/client/check.d.ts +10 -3
  28. package/esm/client/check.js +28 -8
  29. package/esm/client/direct-client.d.ts +49 -29
  30. package/esm/client/direct-client.js +59 -59
  31. package/esm/client/discovery-cache.d.ts +12 -5
  32. package/esm/client/discovery-cache.js +11 -4
  33. package/esm/client/events.d.ts +11 -3
  34. package/esm/client/events.js +1 -1
  35. package/esm/client/mod.d.ts +6 -4
  36. package/esm/client/mod.js +5 -3
  37. package/esm/client/storage.d.ts +53 -23
  38. package/esm/client/storage.js +43 -15
  39. package/esm/errors.d.ts +15 -12
  40. package/esm/errors.js +12 -8
  41. package/esm/identity/delivery.d.ts +7 -7
  42. package/esm/identity/delivery.js +1 -2
  43. package/esm/identity/errors.d.ts +10 -4
  44. package/esm/identity/errors.js +4 -1
  45. package/esm/identity/events.d.ts +21 -5
  46. package/esm/identity/events.js +4 -2
  47. package/esm/identity/external/_shared.d.ts +5 -7
  48. package/esm/identity/external/_shared.js +5 -7
  49. package/esm/identity/external/_token-exchange.d.ts +3 -6
  50. package/esm/identity/external/_token-exchange.js +2 -5
  51. package/esm/identity/external/apple-client-secret.d.ts +13 -8
  52. package/esm/identity/external/apple-client-secret.js +9 -6
  53. package/esm/identity/external/apple.d.ts +17 -10
  54. package/esm/identity/external/apple.js +12 -9
  55. package/esm/identity/external/discord.d.ts +5 -1
  56. package/esm/identity/external/errors.d.ts +6 -2
  57. package/esm/identity/external/errors.js +6 -2
  58. package/esm/identity/external/flow.d.ts +9 -6
  59. package/esm/identity/external/flow.js +6 -5
  60. package/esm/identity/external/github.d.ts +7 -3
  61. package/esm/identity/external/github.js +2 -2
  62. package/esm/identity/external/google.d.ts +5 -1
  63. package/esm/identity/external/oauth2.d.ts +13 -2
  64. package/esm/identity/external/oauth2.js +3 -3
  65. package/esm/identity/external/oidc.d.ts +21 -10
  66. package/esm/identity/external/oidc.js +11 -6
  67. package/esm/identity/external/provider.d.ts +4 -2
  68. package/esm/identity/identifier.d.ts +6 -5
  69. package/esm/identity/identifier.js +6 -5
  70. package/esm/identity/lockout.d.ts +4 -3
  71. package/esm/identity/lockout.js +4 -3
  72. package/esm/identity/mfa/mod.d.ts +2 -1
  73. package/esm/identity/mfa/mod.js +2 -1
  74. package/esm/identity/mfa/recovery-codes.d.ts +2 -1
  75. package/esm/identity/mfa/service.d.ts +29 -11
  76. package/esm/identity/mfa/service.js +35 -11
  77. package/esm/identity/mfa/totp.d.ts +4 -2
  78. package/esm/identity/mfa/totp.js +4 -2
  79. package/esm/identity/migration.d.ts +8 -7
  80. package/esm/identity/migration.js +9 -12
  81. package/esm/identity/mod.d.ts +2 -2
  82. package/esm/identity/mod.js +2 -2
  83. package/esm/identity/otp.d.ts +23 -16
  84. package/esm/identity/otp.js +13 -12
  85. package/esm/identity/password-policy.d.ts +3 -3
  86. package/esm/identity/password-policy.js +3 -3
  87. package/esm/identity/password.d.ts +23 -20
  88. package/esm/identity/password.js +15 -16
  89. package/esm/identity/rate-limit.d.ts +1 -7
  90. package/esm/identity/rate-limit.js +1 -2
  91. package/esm/identity/service.d.ts +76 -44
  92. package/esm/identity/service.js +86 -60
  93. package/esm/identity/session.d.ts +5 -4
  94. package/esm/identity/session.js +5 -4
  95. package/esm/identity/token-flow.d.ts +8 -8
  96. package/esm/identity/token-flow.js +5 -6
  97. package/esm/models/client.d.ts +7 -4
  98. package/esm/models/token.d.ts +2 -3
  99. package/esm/react/callback.d.ts +3 -3
  100. package/esm/react/callback.js +3 -3
  101. package/esm/react/components/class-names.d.ts +3 -6
  102. package/esm/react/components/class-names.js +3 -6
  103. package/esm/react/components/internal.d.ts +7 -17
  104. package/esm/react/components/internal.js +7 -17
  105. package/esm/react/components/mod.d.ts +7 -5
  106. package/esm/react/components/mod.js +7 -5
  107. package/esm/react/components/types.d.ts +8 -6
  108. package/esm/react/context.d.ts +3 -3
  109. package/esm/react/provider.d.ts +3 -2
  110. package/esm/react/provider.js +0 -13
  111. package/esm/react/require-auth.d.ts +0 -3
  112. package/esm/react/require-auth.js +0 -3
  113. package/esm/react/testing.d.ts +3 -2
  114. package/esm/react/testing.js +3 -3
  115. package/esm/react/use-oauth2.d.ts +1 -1
  116. package/esm/server/authorization-server.d.ts +81 -54
  117. package/esm/server/authorization-server.js +76 -100
  118. package/esm/server/grants/authorization-code.d.ts +7 -3
  119. package/esm/server/grants/authorization-code.js +6 -2
  120. package/esm/server/grants/device-authorization.d.ts +1 -1
  121. package/esm/server/grants/grant.d.ts +15 -8
  122. package/esm/server/grants/grant.js +26 -18
  123. package/esm/server/grants/password.d.ts +1 -1
  124. package/esm/server/grants/refresh-token.d.ts +6 -3
  125. package/esm/server/grants/refresh-token.js +4 -3
  126. package/esm/server/introspection-token-reader.d.ts +16 -7
  127. package/esm/server/introspection-token-reader.js +9 -3
  128. package/esm/server/jwks-token-reader.d.ts +3 -3
  129. package/esm/server/jwks-token-reader.js +3 -3
  130. package/esm/server/public-suffix/rules.d.ts +4 -4
  131. package/esm/server/public-suffix/rules.js +4 -4
  132. package/esm/server/redirect-uri.d.ts +6 -9
  133. package/esm/server/redirect-uri.js +7 -34
  134. package/esm/server/resource-server.d.ts +5 -3
  135. package/esm/server/resource-server.js +5 -3
  136. package/esm/server/services/client.d.ts +5 -1
  137. package/esm/server/services/device-authorization.d.ts +6 -6
  138. package/esm/server/services/token.d.ts +15 -8
  139. package/esm/server/services/token.js +1 -1
  140. package/esm/server/signing-keys.d.ts +24 -9
  141. package/esm/server/signing-keys.js +20 -9
  142. package/esm/testing/contract/mod.d.ts +3 -3
  143. package/esm/testing/contract/mod.js +3 -3
  144. package/esm/testing/mod.d.ts +4 -2
  145. package/esm/testing/mod.js +4 -2
  146. package/esm/testing/services.d.ts +1 -1
  147. package/esm/testing/services.js +1 -1
  148. package/esm/testing/tenant.d.ts +13 -4
  149. package/esm/utils/_buffer.d.ts +3 -2
  150. package/esm/utils/_buffer.js +3 -2
  151. package/esm/utils/_timing-safe.d.ts +8 -6
  152. package/esm/utils/_timing-safe.js +8 -6
  153. package/esm/utils/basic-auth.d.ts +5 -12
  154. package/esm/utils/basic-auth.js +5 -12
  155. package/esm/utils/crypto.d.ts +2 -1
  156. package/esm/utils/crypto.js +2 -1
  157. package/esm/utils/text.d.ts +2 -9
  158. package/esm/utils/text.js +2 -9
  159. package/esm/utils/url.d.ts +2 -2
  160. package/esm/utils/url.js +0 -2
  161. package/package.json +1 -1
@@ -106,7 +106,7 @@ const AUTHORIZATION_CODE_GRANT_TYPE = "authorization_code";
106
106
  * these paths — as the Hono adapter's `routes()` does — to match what the
107
107
  * server advertises in its RFC 8414 metadata.
108
108
  *
109
- * Covers the whole served surface: the seven endpoints the server advertises,
109
+ * Covers the whole served surface: the eight endpoints the server can advertise,
110
110
  * plus the two discovery documents (`metadata` for RFC 8414,
111
111
  * `oidcMetadata` for OIDC Discovery) that are served but never advertised.
112
112
  * Pair it with {@link ENDPOINT_METHODS} to enumerate path *and* verbs.
@@ -123,7 +123,7 @@ export const ENDPOINT_PATHS = Object
123
123
  /**
124
124
  * The HTTP methods each endpoint in {@link ENDPOINT_PATHS} answers — `POST` for
125
125
  * the token-style endpoints, `GET` for authorize/JWKS/discovery, both for
126
- * UserInfo. Read it instead of re-deriving verbs per endpoint, so a hand-rolled
126
+ * UserInfo and end-session. Read it instead of re-deriving verbs per endpoint, so a hand-rolled
127
127
  * mount answers exactly what the built-in `routes()` does.
128
128
  *
129
129
  * @example
@@ -148,6 +148,7 @@ function isDeviceAuthorizationGrant(grant) {
148
148
  * - Introspection endpoint (POST /introspect) - RFC 7662
149
149
  * - Device authorization endpoint (POST /device_authorization) - RFC 8628
150
150
  * - Metadata endpoint (GET /.well-known/oauth-authorization-server) - RFC 8414
151
+ * - OIDC discovery, JWKS, UserInfo, and end-session endpoints, when configured
151
152
  *
152
153
  * Framework-agnostic: each `handle*Request` method takes a web `Request` and
153
154
  * returns a `Response`, so an adapter routes its endpoints to them. Extends
@@ -189,7 +190,6 @@ export class AuthorizationServer extends ResourceServer {
189
190
  #userClaims;
190
191
  #introspectionClaims;
191
192
  #canIntrospectToken;
192
- /** Resolves the per-request {@link AuthorizationServerContext}. */
193
193
  #resolve;
194
194
  /**
195
195
  * Creates an authorization server from the given
@@ -203,6 +203,7 @@ export class AuthorizationServer extends ResourceServer {
203
203
  resolve: options.resolve,
204
204
  Scope: options.Scope,
205
205
  realm: options.realm,
206
+ clockSkewSeconds: options.clockSkewSeconds,
206
207
  errorFormat: options.errorFormat,
207
208
  throwOnError: options.throwOnError,
208
209
  });
@@ -251,7 +252,6 @@ export class AuthorizationServer extends ResourceServer {
251
252
  async authorizationContext(request) {
252
253
  return await this.#resolve(request);
253
254
  }
254
- /** Resolves the endpoint URLs for a request (issuer-derived unless set). */
255
255
  #resolvedEndpoints(context) {
256
256
  const { issuer } = context;
257
257
  const endpoints = {};
@@ -309,22 +309,15 @@ export class AuthorizationServer extends ResourceServer {
309
309
  },
310
310
  });
311
311
  }
312
- /**
313
- * Applies `Cache-Control: no-store` and `Pragma: no-cache` headers to an
314
- * error per RFC 6749 Section 5.2. The headers land on both the rethrown
315
- * error (when {@link throwOnError} is true) and the generated response.
316
- */
312
+ /** Sets the RFC 6749 §5.2 `no-store` cache headers on an error. */
317
313
  addCacheHeaders(error) {
318
314
  error.headers.set("Cache-Control", "no-store");
319
315
  error.headers.set("Pragma", "no-cache");
320
316
  }
321
317
  /**
322
- * Error-prepare callback for the endpoints that authenticate the client
323
- * (token, revocation, introspection, device authorization). Attaches the
324
- * RFC 6749 Section 5.2 cache headers, and — when client authentication was
325
- * attempted via the `Authorization` header and failed with a 401 — the
326
- * matching `WWW-Authenticate: Basic` challenge that RFC 6749 Section 5.2
327
- * requires.
318
+ * Error-prepare callback for client-authenticated endpoints: cache headers,
319
+ * plus the RFC 6749 §5.2 `WWW-Authenticate: Basic` challenge on a 401 when
320
+ * the request sent an `Authorization` header.
328
321
  */
329
322
  prepareClientAuthError(request) {
330
323
  return (error) => {
@@ -334,16 +327,11 @@ export class AuthorizationServer extends ResourceServer {
334
327
  }
335
328
  };
336
329
  }
337
- /**
338
- * Rejects a client that is not registered for a grant type
339
- * (RFC 6749 Section 5.2 `unauthorized_client`).
340
- */
341
330
  #assertGrantAuthorized(client, grantType) {
342
331
  if (!client.grants?.includes(grantType)) {
343
332
  throw new UnauthorizedClientError(`client is not authorized to use the ${grantType} grant type`);
344
333
  }
345
334
  }
346
- /** The grant a token request's `grant_type` dispatches to. */
347
335
  #grantFromBody(body) {
348
336
  const grantType = body.get("grant_type");
349
337
  if (typeof grantType !== "string") {
@@ -381,8 +369,10 @@ export class AuthorizationServer extends ResourceServer {
381
369
  * Supports multiple grant types based on the configured grants.
382
370
  *
383
371
  * A malformed request — wrong method, a content-type other than
384
- * `application/x-www-form-urlencoded`, or an unsupported grant — is a 400
385
- * `invalid_request`.
372
+ * `application/x-www-form-urlencoded`, or no `grant_type` — is a 400
373
+ * `invalid_request`. A `grant_type` with no registered grant is
374
+ * `unsupported_grant_type`, checked before client authentication; a client
375
+ * not registered for the grant is `unauthorized_client`.
386
376
  */
387
377
  async handleTokenRequest(request) {
388
378
  try {
@@ -401,6 +391,8 @@ export class AuthorizationServer extends ResourceServer {
401
391
  * Mint an OIDC id_token for a user-bound token whose scope includes
402
392
  * `openid`, or `undefined` when the OIDC surface is off (no signing keys),
403
393
  * the token has no user, the scope lacks `openid`, or no issuer resolved.
394
+ * The id_token's `aud` is the client id and it expires one hour after
395
+ * issue.
404
396
  */
405
397
  async mintIdToken(token, context) {
406
398
  if (!this.signingKeys || !token.user || !context.issuer)
@@ -418,9 +410,10 @@ export class AuthorizationServer extends ResourceServer {
418
410
  };
419
411
  if (token.nonce)
420
412
  claims.nonce = token.nonce;
413
+ else
414
+ delete claims.nonce;
421
415
  return await signJwt(key, claims);
422
416
  }
423
- /** OIDC claims released for a user: `userClaims` under the winning `sub`. */
424
417
  async #oidcClaims(user, scope, authenticationContext) {
425
418
  return {
426
419
  ...(await this.#userClaims?.(user, scope, snapshotAuthenticationContext(authenticationContext)) ?? {}),
@@ -506,8 +499,12 @@ export class AuthorizationServer extends ResourceServer {
506
499
  * `endSession` does the ending and this method does the protocol around it:
507
500
  * identifying the relying party, authorizing the return trip, and shaping
508
501
  * the response. `endSession` is always called, even when nothing else about
509
- * the request checks out, because a logout that refuses to log anyone out is
510
- * the worst possible failure mode for this endpoint.
502
+ * the client, hint, or redirect checks out, because a logout that refuses to
503
+ * log anyone out is the worst possible failure mode for this endpoint. The
504
+ * sole exception is a `client_id` that is not among the audiences of a
505
+ * verified `id_token_hint` sent with it: OIDC RP-Initiated Logout 1.0 §2
506
+ * requires the two to agree, so that request is a 400 `invalid_request`
507
+ * and ends no session.
511
508
  *
512
509
  * **Authorizing `post_logout_redirect_uri` is the whole security surface.**
513
510
  * It is matched by **exact string** against the resolved client's
@@ -519,11 +516,14 @@ export class AuthorizationServer extends ResourceServer {
519
516
  * registered, the browser is **not** sent there — it goes to `fallback` if
520
517
  * the app supplied one, or gets a bare 204.
521
518
  *
522
- * `id_token_hint` is verified against the signing keys but its expiry is
523
- * ignored: by the time anyone logs out the id_token naming their session has
524
- * usually expired, and it is a hint, not authority. `client_id` is accepted
525
- * as the fallback identifier the spec allows. `state` rides along only to a
526
- * URI that passed authorization.
519
+ * `id_token_hint` is verified against the current signing key only — a hint
520
+ * signed by a key since rotated out of signing yields no subject — and its
521
+ * expiry is ignored: by the time anyone logs out the id_token naming their
522
+ * session has usually expired, and it is a hint, not authority. `client_id`,
523
+ * when sent, names the client ahead of the hint's audience, and when both
524
+ * are sent `client_id` must be one of the hint's audiences — a mismatch is
525
+ * a 400 `invalid_request` (OIDC RP-Initiated Logout 1.0 §2). `state` rides
526
+ * along only to a URI that passed authorization.
527
527
  *
528
528
  * Wire the app's session teardown through the `endSession` **constructor
529
529
  * option**; this method answers 404 without it, and the discovery document
@@ -568,12 +568,7 @@ export class AuthorizationServer extends ResourceServer {
568
568
  return this.handleError(error);
569
569
  }
570
570
  }
571
- /**
572
- * The logout parameters, from the query string on GET and the form body on
573
- * POST — RP-Initiated Logout §2 permits both. A verified `id_token_hint`
574
- * contributes the subject and the audience; an unverifiable one contributes
575
- * nothing rather than refusing the logout.
576
- */
571
+ // RP-Initiated Logout §2: parameters come from the GET query or POST body.
577
572
  async #endSessionParameters(request) {
578
573
  const url = new URL(request.url);
579
574
  const params = request.method === "POST"
@@ -581,15 +576,19 @@ export class AuthorizationServer extends ResourceServer {
581
576
  : url.searchParams;
582
577
  const idTokenHint = params.get("id_token_hint");
583
578
  const hint = idTokenHint ? await this.#readIdTokenHint(idTokenHint) : null;
579
+ const clientId = params.get("client_id");
580
+ if (clientId && hint && hint.audiences.length > 0 &&
581
+ !hint.audiences.includes(clientId)) {
582
+ throw new InvalidRequestError("client_id does not match the audience of id_token_hint");
583
+ }
584
584
  return {
585
- clientId: params.get("client_id") ?? hint?.audience ?? null,
585
+ clientId: clientId ?? hint?.audiences[0] ?? null,
586
586
  subject: hint?.subject ?? null,
587
587
  postLogoutRedirectUri: params.get("post_logout_redirect_uri") ?? null,
588
588
  state: params.get("state") ?? null,
589
589
  logoutHint: params.get("logout_hint") ?? null,
590
590
  };
591
591
  }
592
- /** The subject and audience of a signature-valid `id_token_hint`, if any. */
593
592
  async #readIdTokenHint(idTokenHint) {
594
593
  const key = await this.signingKeys?.getSigningKey();
595
594
  if (!key)
@@ -600,17 +599,16 @@ export class AuthorizationServer extends ResourceServer {
600
599
  if (!payload)
601
600
  return null;
602
601
  const aud = payload.aud;
603
- const audience = typeof aud === "string"
604
- ? aud
605
- : Array.isArray(aud) && typeof aud[0] === "string"
606
- ? aud[0]
607
- : null;
602
+ const audiences = typeof aud === "string"
603
+ ? [aud]
604
+ : Array.isArray(aud)
605
+ ? aud.filter((value) => typeof value === "string")
606
+ : [];
608
607
  return {
609
608
  subject: typeof payload.sub === "string" ? payload.sub : null,
610
- audience,
609
+ audiences,
611
610
  };
612
611
  }
613
- /** The client the logout names, or null when it named none we can find. */
614
612
  async #resolveEndSessionClient(request, params) {
615
613
  if (!params.clientId)
616
614
  return null;
@@ -622,11 +620,6 @@ export class AuthorizationServer extends ResourceServer {
622
620
  return null;
623
621
  }
624
622
  }
625
- /**
626
- * The `post_logout_redirect_uri` to honor, with `state` appended — or
627
- * undefined when the request named none, named no resolvable client, or
628
- * named a URI that client did not register.
629
- */
630
623
  #postLogoutRedirect(client, params) {
631
624
  const requested = params.postLogoutRedirectUri;
632
625
  if (!requested || !client)
@@ -643,8 +636,8 @@ export class AuthorizationServer extends ResourceServer {
643
636
  /**
644
637
  * Parses authorization request parameters from the query string.
645
638
  *
646
- * The authorization endpoint is GET-only (RFC 6749 Section 3.1 requires GET
647
- * and only permits POST), so parameters are read from `url.searchParams`.
639
+ * Reads only `url.searchParams`: RFC 6749 Section 3.1 requires GET support
640
+ * and leaves POST optional, and this server serves the endpoint on GET.
648
641
  */
649
642
  parseAuthorizeParameters(request) {
650
643
  const url = new URL(request.url);
@@ -667,10 +660,12 @@ export class AuthorizationServer extends ResourceServer {
667
660
  * Handles an authorization request (GET /authorize).
668
661
  * Used for the authorization code flow.
669
662
  *
670
- * Once the request has named a redirect URI the client registered, failures
671
- * are reported to that URI as an RFC 6749 Section 4.1.2.1 error redirect;
672
- * before that they are error responses from this endpoint, since there is no
673
- * verified place to send the user back to.
663
+ * Once a redirect target is verified — the requested `redirect_uri` matched
664
+ * a registration, or the request omitted it and the client has a literal
665
+ * registration, whose first entry is used — failures are reported to it as
666
+ * an RFC 6749 Section 4.1.2.1 error redirect; before that they are error
667
+ * responses from this endpoint, since there is no verified place to send the
668
+ * user back to.
674
669
  *
675
670
  * Requires a grant extending {@linkcode AuthorizationCodeGrant} registered
676
671
  * under `authorization_code`; without one the endpoint answers 500
@@ -690,11 +685,6 @@ export class AuthorizationServer extends ResourceServer {
690
685
  return this.#authorizeErrorResponse(error, target.url);
691
686
  }
692
687
  }
693
- /**
694
- * The authorize flow proper, recording the redirect target on `target` as
695
- * soon as one is verified so {@link handleAuthorizeRequest} can report later
696
- * failures to the client.
697
- */
698
688
  async #authorize(request, target, authenticateUser, handleConsent) {
699
689
  const params = this.parseAuthorizeParameters(request);
700
690
  const grant = this.#authorizationCodeGrant();
@@ -744,10 +734,6 @@ export class AuthorizationServer extends ResourceServer {
744
734
  redirectUrl.searchParams.set("code", authorizationCode.code);
745
735
  return Response.redirect(redirectUrl.toString(), 302);
746
736
  }
747
- /**
748
- * Reports an authorize failure: an RFC 6749 Section 4.1.2.1 error redirect
749
- * once a verified `redirectUrl` exists, otherwise a plain error response.
750
- */
751
737
  #authorizeErrorResponse(error, redirectUrl) {
752
738
  if (!redirectUrl || !(error instanceof Error) || !isOAuth2Error(error)) {
753
739
  return this.handleError(error);
@@ -790,10 +776,6 @@ export class AuthorizationServer extends ResourceServer {
790
776
  }
791
777
  return grant;
792
778
  }
793
- /**
794
- * The client the authorize request names, once it is registered for the
795
- * authorization code grant.
796
- */
797
779
  async #resolveAuthorizeClient(grant, clientId, services) {
798
780
  if (!clientId) {
799
781
  throw new InvalidRequestError("client_id parameter required");
@@ -802,11 +784,6 @@ export class AuthorizationServer extends ResourceServer {
802
784
  this.#assertGrantAuthorized(client, AUTHORIZATION_CODE_GRANT_TYPE);
803
785
  return client;
804
786
  }
805
- /**
806
- * The URL the browser is sent back to: the requested `redirect_uri` once it
807
- * matches a registration, or the client's first literal registration when the
808
- * request omitted one.
809
- */
810
787
  #resolveRedirectTarget(client, redirectUri) {
811
788
  if (!client.redirectUris?.length) {
812
789
  throw new UnauthorizedClientError("no authorized redirect_uri");
@@ -821,7 +798,6 @@ export class AuthorizationServer extends ResourceServer {
821
798
  }
822
799
  return new URL(redirectUri ?? defaultRedirectUri);
823
800
  }
824
- /** Rejects any `response_type` other than the `code` this endpoint issues. */
825
801
  #validateResponseType(responseType) {
826
802
  if (!responseType) {
827
803
  throw new InvalidRequestError("response_type required");
@@ -831,10 +807,6 @@ export class AuthorizationServer extends ResourceServer {
831
807
  throw new UnsupportedResponseTypeError("response_type not supported");
832
808
  }
833
809
  }
834
- /**
835
- * Rejects PKCE parameters the grant cannot honor (RFC 7636 Section 4.3), and
836
- * a `code_challenge` malformed for a method whose shape Section 4.2 fixes.
837
- */
838
810
  #validateChallenge(grant, params) {
839
811
  const { challenge, challengeMethod } = params;
840
812
  if (challengeMethod && !challenge) {
@@ -847,11 +819,6 @@ export class AuthorizationServer extends ResourceServer {
847
819
  throw new InvalidRequestError("code_challenge must be 43-128 characters using [A-Z] / [a-z] / [0-9] / - / . / _ / ~");
848
820
  }
849
821
  }
850
- /**
851
- * The scope the authorization code will carry: the requested scope narrowed
852
- * by the token service, then by consent when the user has not already
853
- * pre-authorized it. A `Response` short-circuits the flow (a consent page).
854
- */
855
822
  async #resolveScopeAndConsent(options) {
856
823
  const { grant, client, user, requestedScope, authorizedScope, services, handleConsent, } = options;
857
824
  const acceptedScope = await grant.acceptedScope(client, user, requestedScope, services.tokenService);
@@ -972,8 +939,11 @@ export class AuthorizationServer extends ResourceServer {
972
939
  * that field describes how an access token is presented, and its `exp` is
973
940
  * the refresh token's own expiry.
974
941
  *
975
- * A token with no user — the client acting as its own resource owner, as
976
- * the client credentials grant issues — carries no `sub` and no `username`;
942
+ * A token with a user carries `sub` from `subjectOf` — the same value the
943
+ * id_token and UserInfo carry — and `username` when the user has one. A
944
+ * token with no user — the client acting as its own resource owner, as the
945
+ * client credentials grant issues — carries neither, even when
946
+ * {@linkcode AuthorizationServerOptions.introspectionClaims} returns them;
977
947
  * `client_id` identifies the machine. `sub` is optional in RFC 7662, and
978
948
  * leaving it absent keeps `sub` an unambiguous "there is a resource owner"
979
949
  * signal rather than overloading `client_id` into the subject namespace
@@ -1022,16 +992,18 @@ export class AuthorizationServer extends ResourceServer {
1022
992
  if (context.issuer) {
1023
993
  response.iss = context.issuer;
1024
994
  }
1025
- const user = token.user;
1026
- if (user?.id) {
1027
- response.sub = user.id;
1028
- }
1029
- if (user?.username) {
1030
- response.username = user.username;
995
+ if (token.user) {
996
+ response.sub = this.#subjectOf(token.user);
997
+ const username = token.user.username;
998
+ if (typeof username === "string" && username) {
999
+ response.username = username;
1000
+ }
1031
1001
  }
1032
1002
  if (this.#introspectionClaims) {
1003
+ const extensions = await this.#introspectionClaims(token);
1004
+ const { sub: _sub, username: _username, ...machineExtensions } = extensions;
1033
1005
  response = {
1034
- ...await this.#introspectionClaims(token),
1006
+ ...(token.user ? extensions : machineExtensions),
1035
1007
  ...response,
1036
1008
  };
1037
1009
  }
@@ -1118,7 +1090,6 @@ export class AuthorizationServer extends ResourceServer {
1118
1090
  const { challengeMethods } = grant;
1119
1091
  return Object.keys(challengeMethods).filter((method) => typeof challengeMethods[method] === "function");
1120
1092
  }
1121
- /** Publishes the resolved URLs of the OIDC (or plain OAuth2) endpoints. */
1122
1093
  #advertiseEndpoints(metadata, endpoints, oidc) {
1123
1094
  for (const [key, endpoint] of ADVERTISED_ENTRIES) {
1124
1095
  if (endpoint.oidc !== oidc)
@@ -1222,13 +1193,18 @@ export class AuthorizationServer extends ResourceServer {
1222
1193
  * - `POST {revocationEndpoint}` → `handleRevocationRequest`
1223
1194
  * - `POST {introspectionEndpoint}` → `handleIntrospectionRequest`
1224
1195
  * - `POST {deviceAuthorizationEndpoint}` → `handleDeviceAuthorizationRequest`
1225
- * - `GET /.well-known/oauth-authorization-server` → `handleMetadataRequest`
1196
+ * - `GET {jwksEndpoint}` → `handleJwksRequest`
1197
+ * - `GET`/`POST {userinfoEndpoint}` → `handleUserInfoRequest`
1198
+ * - `GET /.well-known/oauth-authorization-server` → `handleMetadataRequest`
1199
+ * - `GET /.well-known/openid-configuration` → `handleOidcMetadataRequest`
1200
+ *
1201
+ * The two discovery paths are matched literally, not against the issuer.
1226
1202
  *
1227
- * The authorization endpoint is deliberately **not** routed: `/authorize`
1228
- * needs an `authenticateUser` (and optional `handleConsent`) callback this
1229
- * transport has no access to, and in real flows it's a browser navigation,
1230
- * never a call through the client's `fetch`. A request to it throws an
1231
- * actionable error pointing at `handleAuthorizeRequest`. Any other path also
1203
+ * The authorization and end-session endpoints are deliberately **not**
1204
+ * routed: in real flows both are browser navigations, never calls through the
1205
+ * client's `fetch`, and `/authorize` needs an `authenticateUser` (and optional
1206
+ * `handleConsent`) callback this transport has no access to. A request to
1207
+ * either throws an error naming the handler to drive directly. Any other path also
1232
1208
  * throws — the helper intentionally does not proxy arbitrary paths; if a call
1233
1209
  * lands here unexpectedly, that's a wiring bug worth surfacing rather than
1234
1210
  * silently forwarding.
@@ -24,7 +24,7 @@ export interface AuthorizationCodeGrantServices<Client extends ClientInterface,
24
24
  * @see https://datatracker.ietf.org/doc/html/rfc7636
25
25
  */
26
26
  export interface AuthorizationCodeGrantOptions<Client extends ClientInterface, User, S extends AbstractScope> extends GrantOptions<Client, User, S, AuthorizationCodeGrantServices<Client, User, S>> {
27
- /** Allow optional refresh token. Defaults to true. */
27
+ /** Whether to issue a refresh token with the access token. Defaults to `true`. */
28
28
  allowRefreshToken?: boolean;
29
29
  /** Custom PKCE challenge methods. Defaults to S256 only. */
30
30
  challengeMethods?: ChallengeMethods;
@@ -166,8 +166,12 @@ export declare class AuthorizationCodeGrant<Client extends ClientInterface, User
166
166
  * Exchanges an authorization code for an access token. If PKCE was used
167
167
  * during authorization, the code_verifier must be provided.
168
168
  *
169
- * The code is claimed by revoking it: only the caller whose revoke wins
170
- * receives a token, so concurrent exchanges of one code cannot both succeed.
169
+ * The code is claimed by revoking it before the client-match, verifier, and
170
+ * `redirect_uri` checks, so a failed exchange still consumes the code. Only
171
+ * the caller whose revoke wins receives a token, given an atomic
172
+ * `authorizationCodeService.revoke`. Presenting a code that already minted
173
+ * tokens asks `tokenService.revokeCode` to revoke those tokens and fails with
174
+ * `invalid_grant` (RFC 6819 §4.4.1.1).
171
175
  *
172
176
  * @throws {InvalidRequestError} When `code` is missing, a required `code_verifier` is absent, or the verifier is malformed.
173
177
  * @throws {InvalidGrantError} When the code is unknown, expired, already claimed, or its verifier or `redirect_uri` does not match.
@@ -164,8 +164,12 @@ export class AuthorizationCodeGrant extends AbstractGrant {
164
164
  * Exchanges an authorization code for an access token. If PKCE was used
165
165
  * during authorization, the code_verifier must be provided.
166
166
  *
167
- * The code is claimed by revoking it: only the caller whose revoke wins
168
- * receives a token, so concurrent exchanges of one code cannot both succeed.
167
+ * The code is claimed by revoking it before the client-match, verifier, and
168
+ * `redirect_uri` checks, so a failed exchange still consumes the code. Only
169
+ * the caller whose revoke wins receives a token, given an atomic
170
+ * `authorizationCodeService.revoke`. Presenting a code that already minted
171
+ * tokens asks `tokenService.revokeCode` to revoke those tokens and fails with
172
+ * `invalid_grant` (RFC 6819 §4.4.1.1).
169
173
  *
170
174
  * @throws {InvalidRequestError} When `code` is missing, a required `code_verifier` is absent, or the verifier is malformed.
171
175
  * @throws {InvalidGrantError} When the code is unknown, expired, already claimed, or its verifier or `redirect_uri` does not match.
@@ -27,7 +27,7 @@ export interface DeviceAuthorizationGrantServices<Client extends ClientInterface
27
27
  * @see https://datatracker.ietf.org/doc/html/rfc8628
28
28
  */
29
29
  export interface DeviceAuthorizationGrantOptions<Client extends ClientInterface, User, S extends AbstractScope> extends GrantOptions<Client, User, S, DeviceAuthorizationGrantServices<Client, User, S>> {
30
- /** Allow optional refresh token. Defaults to true. */
30
+ /** Whether to issue a refresh token with the access token. Defaults to `true`. */
31
31
  allowRefreshToken?: boolean;
32
32
  }
33
33
  /**
@@ -72,7 +72,11 @@ export interface GrantOptions<Client extends ClientInterface, User, Scope extend
72
72
  resolve: (request: Request) => Services | Promise<Services>;
73
73
  /** Scope constructor used to parse scope strings. Defaults to {@linkcode BasicScope}. */
74
74
  Scope?: ScopeConstructor<Scope>;
75
- /** Allow optional refresh token. */
75
+ /**
76
+ * Whether to issue a refresh token with tokens that have a user, when the
77
+ * token service generates one. Defaults to `false`; tokens without a user
78
+ * never get one.
79
+ */
76
80
  allowRefreshToken?: boolean;
77
81
  }
78
82
  /**
@@ -82,8 +86,7 @@ export interface GrantOptions<Client extends ClientInterface, User, Scope extend
82
86
  * interface the grant resolves. A subclass implements {@link token}, reaches
83
87
  * its services through {@link resolveServices}, and overrides
84
88
  * {@link getAuthenticatedClient} only when it authenticates clients by
85
- * something other than their registered credentials (as the authorization-code
86
- * grant does for PKCE).
89
+ * something other than their registered credentials.
87
90
  */
88
91
  export declare abstract class AbstractGrant<Client extends ClientInterface, User, Scope extends AbstractScope = BasicScope, Services extends GrantServices<Client, User, Scope> = GrantServices<Client, User, Scope>> implements DispatchableGrant<Client, User, Scope> {
89
92
  #private;
@@ -91,7 +94,7 @@ export declare abstract class AbstractGrant<Client extends ClientInterface, User
91
94
  abstract readonly grantType: string;
92
95
  /** Constructor used to parse scope strings into {@linkcode Scope} objects. */
93
96
  Scope: ScopeConstructor<Scope>;
94
- /** Allow optional refresh token. Defaults to false. */
97
+ /** Whether tokens with a user are issued a refresh token. */
95
98
  allowRefreshToken: boolean;
96
99
  /** Creates a grant from the shared {@linkcode GrantOptions}. */
97
100
  constructor(options: GrantOptions<Client, User, Scope, Services>);
@@ -142,16 +145,20 @@ export declare abstract class AbstractGrant<Client extends ClientInterface, User
142
145
  * owner whose authorization a rotation could carry forward.
143
146
  * `authenticationContext` is verified event evidence, snapshotted before
144
147
  * generation and carried to storage. Omit it when unknown; it is discarded
145
- * for tokens with no user.
148
+ * for tokens with no user. `familyExpiresAt` is the ceiling of an existing
149
+ * rotation family the token joins (the refresh grant passes it); the
150
+ * expiries are clamped to it before the access token string is generated,
151
+ * so a signed token's `exp` matches the stored expiry.
146
152
  */
147
- generateToken(client: Client, user: User | undefined, scope: Scope | null | undefined, tokenService: TokenServiceInterface<Client, User, Scope>, authenticationContext?: AuthenticationContext): Promise<Token<Client, User, Scope>>;
153
+ generateToken(client: Client, user: User | undefined, scope: Scope | null | undefined, tokenService: TokenServiceInterface<Client, User, Scope>, authenticationContext?: AuthenticationContext, familyExpiresAt?: Date): Promise<Token<Client, User, Scope>>;
148
154
  /**
149
155
  * Authenticates the client presenting this grant's token request against the
150
156
  * grant's own `clientService`, from the credentials the request presents.
151
157
  *
152
158
  * Override to apply grant-specific authentication rules — the
153
- * authorization-code grant accepts a PKCE `code_verifier` in place of a
154
- * client secret for public clients.
159
+ * authorization-code grant, when configured with
160
+ * `requireClientAuthentication: false`, looks up a client that presents a
161
+ * PKCE `code_verifier` by id instead of authenticating it.
155
162
  *
156
163
  * @throws {InvalidClientError} If authentication fails.
157
164
  */
@@ -27,13 +27,12 @@ import { authenticateClientCredentials, extractClientCredentials, } from "../cli
27
27
  * interface the grant resolves. A subclass implements {@link token}, reaches
28
28
  * its services through {@link resolveServices}, and overrides
29
29
  * {@link getAuthenticatedClient} only when it authenticates clients by
30
- * something other than their registered credentials (as the authorization-code
31
- * grant does for PKCE).
30
+ * something other than their registered credentials.
32
31
  */
33
32
  export class AbstractGrant {
34
33
  /** Constructor used to parse scope strings into {@linkcode Scope} objects. */
35
34
  Scope;
36
- /** Allow optional refresh token. Defaults to false. */
35
+ /** Whether tokens with a user are issued a refresh token. */
37
36
  allowRefreshToken;
38
37
  #resolve;
39
38
  /** Creates a grant from the shared {@linkcode GrantOptions}. */
@@ -114,16 +113,16 @@ export class AbstractGrant {
114
113
  * owner whose authorization a rotation could carry forward.
115
114
  * `authenticationContext` is verified event evidence, snapshotted before
116
115
  * generation and carried to storage. Omit it when unknown; it is discarded
117
- * for tokens with no user.
116
+ * for tokens with no user. `familyExpiresAt` is the ceiling of an existing
117
+ * rotation family the token joins (the refresh grant passes it); the
118
+ * expiries are clamped to it before the access token string is generated,
119
+ * so a signed token's `exp` matches the stored expiry.
118
120
  */
119
- async generateToken(client, user, scope, tokenService, authenticationContext) {
121
+ async generateToken(client, user, scope, tokenService, authenticationContext, familyExpiresAt) {
120
122
  const event = user === undefined
121
123
  ? undefined
122
124
  : snapshotAuthenticationContext(authenticationContext);
123
- const token = {
124
- accessToken: await tokenService.generateAccessToken(client, user, scope, event),
125
- client,
126
- };
125
+ const token = { accessToken: "", client };
127
126
  if (event)
128
127
  token.authenticationContext = event;
129
128
  if (user !== undefined)
@@ -133,11 +132,13 @@ export class AbstractGrant {
133
132
  const accessTokenExpiresAt = await tokenService.accessTokenExpiresAt(client, user, scope);
134
133
  if (accessTokenExpiresAt)
135
134
  token.accessTokenExpiresAt = accessTokenExpiresAt;
135
+ let result = token;
136
+ let ceiling = familyExpiresAt;
136
137
  if (this.allowRefreshToken && user !== undefined) {
137
138
  const refreshToken = await tokenService.generateRefreshToken(client, user, scope);
138
139
  if (refreshToken) {
139
140
  const familyCreatedAt = new Date();
140
- const result = {
141
+ const withRefresh = {
141
142
  ...token,
142
143
  refreshToken,
143
144
  familyId: crypto.randomUUID(),
@@ -145,24 +146,31 @@ export class AbstractGrant {
145
146
  };
146
147
  const refreshTokenExpiresAt = await tokenService.refreshTokenExpiresAt(client, user, scope);
147
148
  if (refreshTokenExpiresAt) {
148
- result.refreshTokenExpiresAt = refreshTokenExpiresAt;
149
+ withRefresh.refreshTokenExpiresAt = refreshTokenExpiresAt;
149
150
  }
150
- const familyExpiresAt = await tokenService
151
+ const newFamilyExpiresAt = await tokenService
151
152
  .refreshTokenFamilyExpiresAt?.(client, user, familyCreatedAt, scope);
152
- if (familyExpiresAt)
153
- this.capAtFamilyExpiry(result, familyExpiresAt);
154
- return result;
153
+ if (newFamilyExpiresAt) {
154
+ ceiling = ceiling && ceiling < newFamilyExpiresAt
155
+ ? ceiling
156
+ : newFamilyExpiresAt;
157
+ }
158
+ result = withRefresh;
155
159
  }
156
160
  }
157
- return token;
161
+ if (ceiling)
162
+ this.capAtFamilyExpiry(result, ceiling);
163
+ result.accessToken = await tokenService.generateAccessToken(client, user, scope, event, result.accessTokenExpiresAt);
164
+ return result;
158
165
  }
159
166
  /**
160
167
  * Authenticates the client presenting this grant's token request against the
161
168
  * grant's own `clientService`, from the credentials the request presents.
162
169
  *
163
170
  * Override to apply grant-specific authentication rules — the
164
- * authorization-code grant accepts a PKCE `code_verifier` in place of a
165
- * client secret for public clients.
171
+ * authorization-code grant, when configured with
172
+ * `requireClientAuthentication: false`, looks up a client that presents a
173
+ * PKCE `code_verifier` by id instead of authenticating it.
166
174
  *
167
175
  * @throws {InvalidClientError} If authentication fails.
168
176
  */
@@ -29,7 +29,7 @@ export interface PasswordGrantServices<Client extends ClientInterface, User, S e
29
29
  }
30
30
  /** Options for configuring the password grant. */
31
31
  export interface PasswordGrantOptions<Client extends ClientInterface, User, S extends AbstractScope> extends GrantOptions<Client, User, S, PasswordGrantServices<Client, User, S>> {
32
- /** Allow optional refresh token. Defaults to true. */
32
+ /** Whether to issue a refresh token with the access token. Defaults to `true`. */
33
33
  allowRefreshToken?: boolean;
34
34
  }
35
35
  /**
@@ -30,7 +30,9 @@ export interface TokenReuseEvent<Client extends ClientInterface, User> {
30
30
  export interface RefreshTokenGrantOptions<Client extends ClientInterface, User, S extends AbstractScope> extends Omit<GrantOptions<Client, User, S, RefreshTokenGrantServices<Client, User, S>>, "allowRefreshToken"> {
31
31
  /**
32
32
  * Called when reuse detection catches a rotated-out refresh token being
33
- * replayed (after the family was revoked) — record it as a security event.
33
+ * replayed by the client it was issued to (after the family was revoked) —
34
+ * record it as a security event. Reuse detection runs only when the token
35
+ * service implements both `getRevokedRefreshToken` and `revokeFamily`.
34
36
  *
35
37
  * Trigger point `token-reuse` (see `docs/trigger-points.md`).
36
38
  * Fire-and-forget: it is awaited, but a throw is caught and logged, never
@@ -71,8 +73,9 @@ export declare class RefreshTokenGrant<Client extends ClientInterface, User, S e
71
73
  * authorization.
72
74
  *
73
75
  * @throws {InvalidRequestError} If `refresh_token` is missing.
74
- * @throws {InvalidGrantError} If the refresh token is unknown or expired, or
75
- * its family has outlived the absolute cap.
76
+ * @throws {InvalidGrantError} If the refresh token is unknown or expired, its
77
+ * family has outlived the absolute cap, or a concurrent exchange already
78
+ * claimed it.
76
79
  * @throws {InvalidClientError} If the token was issued to another client.
77
80
  * @throws {InvalidScopeError} If the requested scope exceeds the original grant.
78
81
  */