@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
@@ -149,15 +149,12 @@ export declare function checkRedirectUriPattern(pattern: string, isPublicSuffix?
149
149
  * - A host label may never be empty, and a single trailing dot is treated as
150
150
  * the same host, so `example.com.` cannot smuggle an extra label past the
151
151
  * parent-domain rule.
152
- * - A requested URI containing `*` never matches anything, checked both on the
153
- * raw string and on the percent-decoded host, so a pattern can never be
154
- * replayed back as a literal redirect target.
155
- *
156
- * That last rule is a deliberate tightening: `*` is a legal host character, so
157
- * a registration containing one that was previously matched by exact string
158
- * comparison no longer matches at all. Such a registration cannot be created
159
- * any more — a `*` now makes the value a pattern, which must satisfy the rules
160
- * above — so this only affects rows written before patterns existed.
152
+ * - A requested URI containing a literal `*` never matches any registration,
153
+ * so a pattern can never be replayed back as a redirect target. A requested
154
+ * host that percent-decodes to one containing `*` never matches a pattern.
155
+ *
156
+ * Any registration containing `*` is a pattern, so a stored literal URI with a
157
+ * `*` in its host matches only if it also satisfies the pattern rules.
161
158
  *
162
159
  * @param registered The client's registered redirect URIs.
163
160
  * @param redirectUri The `redirect_uri` from the authorization request.
@@ -38,27 +38,8 @@
38
38
  *
39
39
  * @module
40
40
  */
41
- /**
42
- * A label used to ask whether the *children* of a parent domain are public
43
- * suffixes, which is a different question from whether the parent is one.
44
- *
45
- * `r.appspot.com` is a registrable domain by the list's reckoning, but the rule
46
- * `*.r.appspot.com` makes every one of its subdomains a public suffix belonging
47
- * to a different registrant — so a wildcard over it hands codes to strangers
48
- * even though the parent itself passes. Probing with a label no registry has
49
- * ever issued answers that in one lookup.
50
- */
41
+ // A label no registry issues; a PSL hit on it catches `*.r.appspot.com` rules.
51
42
  const CHILD_PROBE_LABEL = "udibo-public-suffix-probe";
52
- /**
53
- * Split a hostname into labels, or `null` when it is not usable for matching.
54
- *
55
- * A single trailing dot is the DNS root and is dropped, so `example.com.` and
56
- * `example.com` are the same host — without this, the extra empty label would
57
- * shift every label the public-suffix check looks at and let `*.deno.net.`
58
- * through. Any *other* empty label (`a..b`, a leading `.`) is refused outright
59
- * rather than normalized: it cannot resolve, and an empty leftmost label would
60
- * otherwise satisfy any prefix/suffix test.
61
- */
62
43
  function hostLabels(hostname) {
63
44
  const normalized = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
64
45
  if (normalized.length === 0)
@@ -175,12 +156,7 @@ export function checkRedirectUriPattern(pattern, isPublicSuffix) {
175
156
  }
176
157
  return null;
177
158
  }
178
- /**
179
- * The loopback IP literals RFC 8252 §7.3 covers, in the form
180
- * `URL.hostname` normalizes them to. `localhost` is deliberately absent: RFC
181
- * 8252 §8.3 advises against it because its resolution depends on a host file
182
- * the authorization server does not control.
183
- */
159
+ // RFC 8252 §7.3 IP literals only; `localhost` is excluded per RFC 8252 §8.3.
184
160
  const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "[::1]"]);
185
161
  function matchesLoopback(registration, redirectUri) {
186
162
  if (isRedirectUriPattern(registration))
@@ -275,15 +251,12 @@ function matchesPattern(pattern, redirectUri, isPublicSuffix) {
275
251
  * - A host label may never be empty, and a single trailing dot is treated as
276
252
  * the same host, so `example.com.` cannot smuggle an extra label past the
277
253
  * parent-domain rule.
278
- * - A requested URI containing `*` never matches anything, checked both on the
279
- * raw string and on the percent-decoded host, so a pattern can never be
280
- * replayed back as a literal redirect target.
254
+ * - A requested URI containing a literal `*` never matches any registration,
255
+ * so a pattern can never be replayed back as a redirect target. A requested
256
+ * host that percent-decodes to one containing `*` never matches a pattern.
281
257
  *
282
- * That last rule is a deliberate tightening: `*` is a legal host character, so
283
- * a registration containing one that was previously matched by exact string
284
- * comparison no longer matches at all. Such a registration cannot be created
285
- * any more — a `*` now makes the value a pattern, which must satisfy the rules
286
- * above — so this only affects rows written before patterns existed.
258
+ * Any registration containing `*` is a pattern, so a stored literal URI with a
259
+ * `*` in its host matches only if it also satisfies the pattern rules.
287
260
  *
288
261
  * @param registered The client's registered redirect URIs.
289
262
  * @param redirectUri The `redirect_uri` from the authorization request.
@@ -231,10 +231,12 @@ export declare class ResourceServer<Client extends ClientInterface, User, S exte
231
231
  *
232
232
  * The token's expiry is compared against now with {@linkcode
233
233
  * clockSkewSeconds} of leeway, so the same tolerance applies whichever
234
- * token reader produced the token.
234
+ * token reader produced the token. A token without `accessTokenExpiresAt`
235
+ * is never refused as expired here. Revocation is the reader's to report,
236
+ * by resolving no token.
235
237
  *
236
- * @throws {InvalidTokenError} If the token is invalid, expired beyond the
237
- * configured leeway, or revoked
238
+ * @throws {InvalidTokenError} If the reader resolves no token, or the token
239
+ * has expired beyond the configured leeway
238
240
  * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
239
241
  */
240
242
  getToken(accessToken: string, services: ResourceServerServices<Client, User, S>): Promise<Token<Client, User, S>>;
@@ -258,10 +258,12 @@ export class ResourceServer {
258
258
  *
259
259
  * The token's expiry is compared against now with {@linkcode
260
260
  * clockSkewSeconds} of leeway, so the same tolerance applies whichever
261
- * token reader produced the token.
261
+ * token reader produced the token. A token without `accessTokenExpiresAt`
262
+ * is never refused as expired here. Revocation is the reader's to report,
263
+ * by resolving no token.
262
264
  *
263
- * @throws {InvalidTokenError} If the token is invalid, expired beyond the
264
- * configured leeway, or revoked
265
+ * @throws {InvalidTokenError} If the reader resolves no token, or the token
266
+ * has expired beyond the configured leeway
265
267
  * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
266
268
  */
267
269
  async getToken(accessToken, services) {
@@ -30,6 +30,10 @@ export interface ClientServiceInterface<Client extends ClientInterface, User> {
30
30
  * carried none.
31
31
  */
32
32
  getAuthenticated(id: string, secret?: string): Promise<Client | undefined>;
33
- /** Retrieves a user associated with a client (for client-credentials grant). */
33
+ /**
34
+ * Resolves the user a client-credentials token acts for. Resolving
35
+ * `undefined` is the conformant case and yields a user-less machine token;
36
+ * see `ClientCredentialsGrant` before mapping a client to a human user.
37
+ */
34
38
  getUser(client: Client | string): Promise<User | undefined>;
35
39
  }
@@ -34,15 +34,15 @@ import type { AbstractScope } from "../../models/scope.js";
34
34
  */
35
35
  export interface DeviceAuthorizationServiceInterface<Client extends ClientInterface, User, Scope extends AbstractScope> {
36
36
  /**
37
- * Lifetime of device codes in seconds.
38
- * RFC 8628 does not specify a default, but 15-30 minutes is common.
39
- * @default 1800 (30 minutes)
37
+ * Lifetime of device codes in seconds. RFC 8628 sets no default; 15–30
38
+ * minutes is common.
40
39
  */
41
40
  lifetime: number;
42
41
  /**
43
- * Minimum polling interval in seconds.
44
- * Clients should not poll more frequently than this.
45
- * @default 5
42
+ * Minimum polling interval in seconds, copied onto each new authorization
43
+ * and returned to the device as `interval`. The grant answers a faster poll
44
+ * with `slow_down` and adds 5 seconds to that authorization's interval
45
+ * (RFC 8628 §3.5).
46
46
  */
47
47
  interval: number;
48
48
  /**
@@ -24,9 +24,10 @@ export interface TokenReaderInterface<Client extends ClientInterface, User, Scop
24
24
  * its grant types.
25
25
  *
26
26
  * **A token need not have a user.** The client credentials grant (RFC 6749
27
- * §4.4) has no resource owner, so it calls `acceptedScope`,
28
- * `generateAccessToken`, and `accessTokenExpiresAt` with `user: undefined` and
29
- * saves a {@linkcode Token} whose `user` is unset. An implementation that
27
+ * §4.4) has no resource owner, so when `ClientServiceInterface.getUser`
28
+ * resolves none it calls `acceptedScope`, `generateAccessToken`, and
29
+ * `accessTokenExpiresAt` with `user: undefined` and saves a {@linkcode Token}
30
+ * whose `user` is unset. An implementation that
30
31
  * dereferences `user` without checking will throw on those requests, and a
31
32
  * store with a non-nullable user column cannot persist the result — decide
32
33
  * scope and lifetime from the client alone when it is absent.
@@ -53,8 +54,13 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
53
54
  * generator names the client as the subject then, per RFC 9068 §2.2.
54
55
  * Forward `authenticationContext` to a signed-token generator so it uses
55
56
  * the credential's event, including the original event on refresh.
57
+ * `expiresAt` is the expiry the grant will store for this token — already
58
+ * clamped to the refresh family's cap when one applies. Forward it too, so
59
+ * a signed token's `exp` never outlives what the server itself honors; the
60
+ * grants always pass it, and a wrapper that drops it leaves offline
61
+ * verifiers accepting the token past its stored expiry.
56
62
  */
57
- generateAccessToken(client: Client, user: User | undefined, scope?: Scope | null, authenticationContext?: AuthenticationContext): Promise<string>;
63
+ generateAccessToken(client: Client, user: User | undefined, scope?: Scope | null, authenticationContext?: AuthenticationContext, expiresAt?: Date): Promise<string>;
58
64
  /** Generates a refresh token string. */
59
65
  generateRefreshToken(client: Client, user: User, scope?: Scope | null): Promise<string | undefined>;
60
66
  /**
@@ -71,7 +77,8 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
71
77
  *
72
78
  * **This method is the whole cap.** The grants clamp a rotation's access and
73
79
  * refresh expiries to the date it returns and answer `invalid_grant` once
74
- * that date has passed. Return `undefined` — or leave the method off
80
+ * that date has passed; a refresh-token record without `familyCreatedAt` is
81
+ * never capped. Return `undefined` — or leave the method off
75
82
  * entirely, which is the default — and the family is never capped, however
76
83
  * the service is otherwise configured. Implement it to cap per client (see
77
84
  * {@linkcode AbstractTokenService.refreshTokenMaxLifetime} for the
@@ -151,8 +158,8 @@ export interface AbstractTokenServiceOptions<Client extends ClientInterface, Use
151
158
  /**
152
159
  * Absolute lifetime of a refresh-token family in seconds, read by this
153
160
  * class's {@linkcode AbstractTokenService.refreshTokenFamilyExpiresAt}. Omit
154
- * for the historical behavior, an uncapped sliding window. Must be at least
155
- * `refreshTokenLifetime`.
161
+ * to leave families uncapped, so rotation extends a family indefinitely.
162
+ * Must be at least `refreshTokenLifetime`.
156
163
  */
157
164
  refreshTokenMaxLifetime?: number;
158
165
  /** Client service for hydrating tokens. */
@@ -202,7 +209,7 @@ export declare abstract class AbstractTokenService<Client extends ClientInterfac
202
209
  */
203
210
  acceptedScope(_client: Client, _user: User | undefined, scope?: Scope | null): Promise<Scope | null | undefined | false>;
204
211
  /** Generates an opaque access token string. Override to issue a different format. */
205
- generateAccessToken(_client: Client, _user: User | undefined, _scope?: Scope | null, _authenticationContext?: AuthenticationContext): Promise<string>;
212
+ generateAccessToken(_client: Client, _user: User | undefined, _scope?: Scope | null, _authenticationContext?: AuthenticationContext, _expiresAt?: Date): Promise<string>;
206
213
  /** Generates an opaque refresh token string. Override to issue a different format. */
207
214
  generateRefreshToken(_client: Client, _user: User, _scope?: Scope | null): Promise<string | undefined>;
208
215
  /** Computes the access token expiry as now plus {@linkcode accessTokenLifetime}. */
@@ -52,7 +52,7 @@ export class AbstractTokenService {
52
52
  return Promise.resolve(scope);
53
53
  }
54
54
  /** Generates an opaque access token string. Override to issue a different format. */
55
- generateAccessToken(_client, _user, _scope, _authenticationContext) {
55
+ generateAccessToken(_client, _user, _scope, _authenticationContext, _expiresAt) {
56
56
  return Promise.resolve(crypto.randomUUID());
57
57
  }
58
58
  /** Generates an opaque refresh token string. Override to issue a different format. */
@@ -115,7 +115,11 @@ export declare function generateSigningKey(): Promise<SigningKey>;
115
115
  export declare function exportSigningKeyJwk(key: SigningKey): Promise<JsonWebKey & {
116
116
  kid: string;
117
117
  }>;
118
- /** Import a signing key previously exported with {@link exportSigningKeyJwk}. */
118
+ /**
119
+ * Import a signing key previously exported with {@link exportSigningKeyJwk}.
120
+ * A JWK without a `kid` is given a random one, so every instance importing it
121
+ * would publish and stamp a different `kid` — persist the exported `kid`.
122
+ */
119
123
  export declare function importSigningKeyJwk(jwk: JsonWebKey & {
120
124
  kid?: string;
121
125
  }): Promise<SigningKey>;
@@ -139,10 +143,12 @@ export interface VerifyJwtOptions {
139
143
  }
140
144
  /**
141
145
  * Verify a compact JWS against a public JWK and return its payload, or
142
- * `undefined` when the signature (or shape) is invalid or the payload's `exp`
143
- * is in the past. Exported for tests and lightweight in-process verification;
144
- * production resource servers should verify against the JWKS endpoint with
145
- * their JOSE library of choice.
146
+ * `undefined` when the signature (or shape) is invalid or a numeric `exp` is
147
+ * not in the future. It verifies ES256 regardless of the header's `alg` and
148
+ * checks nothing else: a payload with no `exp` passes, and `iss`, `aud`, and
149
+ * `nbf` are the caller's to check. Exported for tests and lightweight
150
+ * in-process verification; production resource servers should verify against
151
+ * the JWKS endpoint with their JOSE library of choice.
146
152
  */
147
153
  export declare function verifyJwt(jwt: string, publicJwk: JsonWebKey, options?: VerifyJwtOptions): Promise<Record<string, unknown> | undefined>;
148
154
  /**
@@ -159,8 +165,10 @@ export declare function verifyJwt(jwt: string, publicJwk: JsonWebKey, options?:
159
165
  * }
160
166
  * ```
161
167
  *
162
- * The token store still persists the (hashed) JWT string, so revocation and
163
- * introspection keep working exactly as with opaque tokens.
168
+ * Your token store persists the JWT string like any access token, so
169
+ * revocation and introspection work as with opaque tokens for readers that
170
+ * consult the store. A resource server that verifies the JWT offline against
171
+ * JWKS does not see a revocation and accepts the token until its `exp`.
164
172
  */
165
173
  export declare function createJwtAccessTokenGenerator(options: {
166
174
  /** Keys the tokens are signed with (also served from JWKS). */
@@ -173,7 +181,14 @@ export declare function createJwtAccessTokenGenerator(options: {
173
181
  * the client id, which single-resource validators must then accept.
174
182
  */
175
183
  audience?: string;
176
- /** Token lifetime in seconds for the `exp` claim. Defaults to 3600. */
184
+ /**
185
+ * Upper bound in seconds on the `exp` claim. Defaults to 3600. The claim
186
+ * is the earlier of this and the expiry the grant passes as the fifth
187
+ * argument — the token service's `accessTokenExpiresAt`, already clamped to
188
+ * any refresh-family cap — so an offline verifier never accepts the token
189
+ * past the server's own expiry. A wrapper that drops that argument falls
190
+ * back to this lifetime alone.
191
+ */
177
192
  lifetimeSeconds?: number;
178
193
  /** Maps a user to the `sub` claim. Defaults to the user's `id` property. */
179
194
  subjectOf?: (user: unknown) => string;
@@ -198,4 +213,4 @@ export declare function createJwtAccessTokenGenerator(options: {
198
213
  id: string;
199
214
  }, user: unknown, scope?: {
200
215
  toString(): string;
201
- } | null, authenticationContext?: AuthenticationContext) => Promise<string>;
216
+ } | null, authenticationContext?: AuthenticationContext, expiresAt?: Date) => Promise<string>;
@@ -128,7 +128,11 @@ export async function exportSigningKeyJwk(key) {
128
128
  const jwk = await crypto.subtle.exportKey("jwk", key.privateKey);
129
129
  return { ...jwk, kid: key.kid, alg: key.alg };
130
130
  }
131
- /** Import a signing key previously exported with {@link exportSigningKeyJwk}. */
131
+ /**
132
+ * Import a signing key previously exported with {@link exportSigningKeyJwk}.
133
+ * A JWK without a `kid` is given a random one, so every instance importing it
134
+ * would publish and stamp a different `kid` — persist the exported `kid`.
135
+ */
132
136
  export async function importSigningKeyJwk(jwk) {
133
137
  const privateKey = await crypto.subtle.importKey("jwk", jwk, ES256_PARAMS, true, ["sign"]);
134
138
  const kid = jwk.kid ?? crypto.randomUUID();
@@ -150,10 +154,12 @@ export async function signJwt(key, payload, header = {}) {
150
154
  }
151
155
  /**
152
156
  * Verify a compact JWS against a public JWK and return its payload, or
153
- * `undefined` when the signature (or shape) is invalid or the payload's `exp`
154
- * is in the past. Exported for tests and lightweight in-process verification;
155
- * production resource servers should verify against the JWKS endpoint with
156
- * their JOSE library of choice.
157
+ * `undefined` when the signature (or shape) is invalid or a numeric `exp` is
158
+ * not in the future. It verifies ES256 regardless of the header's `alg` and
159
+ * checks nothing else: a payload with no `exp` passes, and `iss`, `aud`, and
160
+ * `nbf` are the caller's to check. Exported for tests and lightweight
161
+ * in-process verification; production resource servers should verify against
162
+ * the JWKS endpoint with their JOSE library of choice.
157
163
  */
158
164
  export async function verifyJwt(jwt, publicJwk, options = {}) {
159
165
  const parts = jwt.split(".");
@@ -190,13 +196,15 @@ export async function verifyJwt(jwt, publicJwk, options = {}) {
190
196
  * }
191
197
  * ```
192
198
  *
193
- * The token store still persists the (hashed) JWT string, so revocation and
194
- * introspection keep working exactly as with opaque tokens.
199
+ * Your token store persists the JWT string like any access token, so
200
+ * revocation and introspection work as with opaque tokens for readers that
201
+ * consult the store. A resource server that verifies the JWT offline against
202
+ * JWKS does not see a revocation and accepts the token until its `exp`.
195
203
  */
196
204
  export function createJwtAccessTokenGenerator(options) {
197
205
  const lifetime = options.lifetimeSeconds ?? 3600;
198
206
  const subjectOf = options.subjectOf ?? defaultSubjectOf;
199
- return async (client, user, scope, authenticationContext) => {
207
+ return async (client, user, scope, authenticationContext, expiresAt) => {
200
208
  const event = user
201
209
  ? snapshotAuthenticationContext(authenticationContext)
202
210
  : undefined;
@@ -205,6 +213,9 @@ export function createJwtAccessTokenGenerator(options) {
205
213
  const extraClaims = user && options.userClaims
206
214
  ? await options.userClaims(user, scope, client, event)
207
215
  : undefined;
216
+ const storedExp = expiresAt
217
+ ? Math.floor(expiresAt.getTime() / 1000)
218
+ : now + lifetime;
208
219
  const payload = {
209
220
  ...extraClaims,
210
221
  iss: options.issuer,
@@ -212,7 +223,7 @@ export function createJwtAccessTokenGenerator(options) {
212
223
  client_id: client.id,
213
224
  aud: options.audience ?? client.id,
214
225
  iat: now,
215
- exp: now + lifetime,
226
+ exp: Math.min(now + lifetime, storedExp),
216
227
  jti: crypto.randomUUID(),
217
228
  };
218
229
  const scopeText = scope?.toString();
@@ -19,9 +19,9 @@
19
19
  * sequential case and fails the concurrent one, which is the whole
20
20
  * reason the suites exist.
21
21
  *
22
- * Each runner takes a factory + seed-helpers because `add` / `register`
23
- * methods aren't part of the public service interface — the consumer
24
- * tells the runner how to populate test data.
22
+ * The service runners take a factory plus seed helpers because `add` /
23
+ * `register` methods aren't part of the public service interfaces — the
24
+ * consumer tells the runner how to populate test data.
25
25
  *
26
26
  * @module
27
27
  */
@@ -19,9 +19,9 @@
19
19
  * sequential case and fails the concurrent one, which is the whole
20
20
  * reason the suites exist.
21
21
  *
22
- * Each runner takes a factory + seed-helpers because `add` / `register`
23
- * methods aren't part of the public service interface — the consumer
24
- * tells the runner how to populate test data.
22
+ * The service runners take a factory plus seed helpers because `add` /
23
+ * `register` methods aren't part of the public service interfaces — the
24
+ * consumer tells the runner how to populate test data.
25
25
  *
26
26
  * @module
27
27
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Testing helpers for apps that use `@udibo/oauth2`.
3
3
  *
4
- * Two layers ship:
4
+ * Three layers ship:
5
5
  *
6
6
  * - **Testing surface for the OAuth2 protocol.** The recommended pattern
7
7
  * for testing apps that use the resource server is to expose your
@@ -20,7 +20,9 @@
20
20
  * - **In-memory service implementations.** `MemoryUserService`,
21
21
  * `MemoryClientService`, `MemoryTokenService`,
22
22
  * `MemoryAuthorizationCodeService`, and `MemoryDeviceAuthorizationService`
23
- * satisfy every server-side service contract, generic over the consumer's
23
+ * implement the authorization server's user, client, token,
24
+ * authorization-code and device-authorization service interfaces, generic
25
+ * over the consumer's
24
26
  * user / client / scope types. Suitable for tests, examples, and
25
27
  * prototypes — never for production.
26
28
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Testing helpers for apps that use `@udibo/oauth2`.
3
3
  *
4
- * Two layers ship:
4
+ * Three layers ship:
5
5
  *
6
6
  * - **Testing surface for the OAuth2 protocol.** The recommended pattern
7
7
  * for testing apps that use the resource server is to expose your
@@ -20,7 +20,9 @@
20
20
  * - **In-memory service implementations.** `MemoryUserService`,
21
21
  * `MemoryClientService`, `MemoryTokenService`,
22
22
  * `MemoryAuthorizationCodeService`, and `MemoryDeviceAuthorizationService`
23
- * satisfy every server-side service contract, generic over the consumer's
23
+ * implement the authorization server's user, client, token,
24
+ * authorization-code and device-authorization service interfaces, generic
25
+ * over the consumer's
24
26
  * user / client / scope types. Suitable for tests, examples, and
25
27
  * prototypes — never for production.
26
28
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * In-memory implementations of every OAuth2 service interface.
2
+ * In-memory implementations of the authorization server's service interfaces.
3
3
  *
4
4
  * The `Memory*Service` family is the public, generic version of the fixtures
5
5
  * the library uses for its own tests. They satisfy the
@@ -1,5 +1,5 @@
1
1
  /**
2
- * In-memory implementations of every OAuth2 service interface.
2
+ * In-memory implementations of the authorization server's service interfaces.
3
3
  *
4
4
  * The `Memory*Service` family is the public, generic version of the fixtures
5
5
  * the library uses for its own tests. They satisfy the
@@ -124,9 +124,17 @@ export interface FakeTenant {
124
124
  addUser(user: FakeTenantUser): Promise<void>;
125
125
  /** Adds an organization. Throws on a duplicate id. */
126
126
  addOrganization(organization: FakeTenantOrganization): void;
127
- /** Makes a person a member of an organization, replacing any earlier membership. */
127
+ /**
128
+ * Makes a person a member of an organization, replacing any earlier
129
+ * membership. Throws when the organization does not exist.
130
+ */
128
131
  addMember(organizationId: string, userId: string, membership?: FakeTenantMembership): void;
129
- /** Ends a membership. Credentials issued in that organization stop answering for it. */
132
+ /**
133
+ * Ends a membership. From then on introspection and `/api/check` stop
134
+ * answering for that organization on credentials issued in it, and UserInfo
135
+ * drops its `org_*` claims; a JWT access token or id_token already minted
136
+ * keeps its claims until it expires.
137
+ */
130
138
  removeMember(organizationId: string, userId: string): void;
131
139
  /** Registers a resource type, so `/api/check` accepts it. */
132
140
  registerResourceType(type: string): void;
@@ -134,12 +142,13 @@ export interface FakeTenant {
134
142
  grant(grant: FakeTenantGrant): void;
135
143
  /**
136
144
  * Chooses who the next authorization requests authenticate as, until
137
- * called again. With nobody chosen, authorize answers `login_required`.
145
+ * called again. With nobody chosen, authorize answers `access_denied`.
138
146
  */
139
147
  signInAs(userId: string | null, signIn?: FakeTenantSignIn): void;
140
148
  /**
141
149
  * Mints an access token without the browser flow, for testing an API
142
- * directly. JWT or opaque as the client is registered.
150
+ * directly. JWT or opaque as the client is registered. Throws when the
151
+ * client or user does not exist.
143
152
  */
144
153
  issueAccessToken(request: FakeTenantTokenRequest): Promise<string>;
145
154
  }
@@ -4,7 +4,8 @@
4
4
  * @module
5
5
  */
6
6
  /**
7
- * Copy to a standalone `ArrayBuffer` to satisfy `BufferSource` on runtimes that
8
- * narrow `Uint8Array<ArrayBufferLike>` away from `ArrayBuffer`.
7
+ * Copies the viewed bytes into a standalone `ArrayBuffer`, for passing a
8
+ * `Uint8Array<ArrayBufferLike>` where Web Crypto's `BufferSource` type does not
9
+ * accept it.
9
10
  */
10
11
  export declare function toArrayBuffer(bytes: Uint8Array): ArrayBuffer;
@@ -4,8 +4,9 @@
4
4
  * @module
5
5
  */
6
6
  /**
7
- * Copy to a standalone `ArrayBuffer` to satisfy `BufferSource` on runtimes that
8
- * narrow `Uint8Array<ArrayBufferLike>` away from `ArrayBuffer`.
7
+ * Copies the viewed bytes into a standalone `ArrayBuffer`, for passing a
8
+ * `Uint8Array<ArrayBufferLike>` where Web Crypto's `BufferSource` type does not
9
+ * accept it.
9
10
  */
10
11
  export function toArrayBuffer(bytes) {
11
12
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Internal constant-time matching shared by the MFA and BFF code paths.
2
+ * Internal timing-safe matching for secret candidates.
3
3
  *
4
4
  * @module
5
5
  */
6
6
  /**
7
- * Constant-time scan for `target` among `candidates`: every candidate is
8
- * compared (no early exit), so timing reveals neither whether nor where a
9
- * match occurred. Returns the index of the *last* matching candidate, or
10
- * `undefined` when none match — order candidates least- to most-preferred so
11
- * a duplicate resolves to the preferred entry.
7
+ * Compares `target` against every candidate with no early exit, so the scan's
8
+ * duration does not reveal where a match occurred. Each comparison is
9
+ * {@link timingSafeEqualString}, which returns early on a byte-length mismatch,
10
+ * so candidates of a different length than `target` are distinguishable by
11
+ * timing. Returns the index of the *last* matching candidate, or `undefined`
12
+ * when none match — order candidates least- to most-preferred so a duplicate
13
+ * resolves to the preferred entry.
12
14
  */
13
15
  export declare function timingSafeMatchIndex(candidates: string[], target: string): number | undefined;
@@ -1,15 +1,17 @@
1
1
  /**
2
- * Internal constant-time matching shared by the MFA and BFF code paths.
2
+ * Internal timing-safe matching for secret candidates.
3
3
  *
4
4
  * @module
5
5
  */
6
6
  import { timingSafeEqualString } from "./crypto.js";
7
7
  /**
8
- * Constant-time scan for `target` among `candidates`: every candidate is
9
- * compared (no early exit), so timing reveals neither whether nor where a
10
- * match occurred. Returns the index of the *last* matching candidate, or
11
- * `undefined` when none match — order candidates least- to most-preferred so
12
- * a duplicate resolves to the preferred entry.
8
+ * Compares `target` against every candidate with no early exit, so the scan's
9
+ * duration does not reveal where a match occurred. Each comparison is
10
+ * {@link timingSafeEqualString}, which returns early on a byte-length mismatch,
11
+ * so candidates of a different length than `target` are distinguishable by
12
+ * timing. Returns the index of the *last* matching candidate, or `undefined`
13
+ * when none match — order candidates least- to most-preferred so a duplicate
14
+ * resolves to the preferred entry.
13
15
  */
14
16
  export function timingSafeMatchIndex(candidates, target) {
15
17
  let index;
@@ -18,9 +18,8 @@ export interface BasicAuth {
18
18
  * trip. A segment that is not valid percent-encoding is taken verbatim, so a
19
19
  * client that sends such characters un-encoded still authenticates.
20
20
  *
21
- * @param authorization The Authorization header value
22
- * @returns The parsed credentials
23
- * @throws {InvalidClientError} If the header is missing or malformed
21
+ * @throws {InvalidClientError} If the header is missing, not `Basic`, not
22
+ * valid base64, or does not decode to `name:pass` with a non-empty name.
24
23
  * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
25
24
  */
26
25
  export declare function parseBasicAuth(authorization: string | null): BasicAuth;
@@ -33,18 +32,12 @@ export declare function parseBasicAuth(authorization: string | null): BasicAuth;
33
32
  * only of `A-Z`, `a-z`, `0-9`, `-`, `_`, `.` and `*` encode byte-identically to
34
33
  * their raw form; every other character, `~` included, is percent-encoded.
35
34
  *
36
- * @param clientId The client ID
37
- * @param clientSecret The client secret
38
- * @returns The encoded Authorization header value
35
+ * @returns The `Basic …` Authorization header value.
39
36
  * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
40
37
  */
41
38
  export declare function encodeBasicAuth(clientId: string, clientSecret: string): string;
42
39
  /**
43
- * Tries to parse basic authentication credentials from an Authorization header.
44
- * Returns undefined if the header is missing or not Basic auth. Decodes both
45
- * segments the same way {@link parseBasicAuth} does.
46
- *
47
- * @param authorization The Authorization header value
48
- * @returns The parsed credentials or undefined
40
+ * Non-throwing {@link parseBasicAuth}: returns `undefined` wherever that would
41
+ * throw — a missing, non-`Basic`, badly encoded or malformed header.
49
42
  */
50
43
  export declare function tryParseBasicAuth(authorization: string | null): BasicAuth | undefined;