@udibo/oauth2 0.4.1 → 0.5.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 (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/redirect-uri.d.ts +6 -9
  131. package/esm/server/redirect-uri.js +7 -34
  132. package/esm/server/resource-server.d.ts +5 -3
  133. package/esm/server/resource-server.js +5 -3
  134. package/esm/server/services/client.d.ts +5 -1
  135. package/esm/server/services/device-authorization.d.ts +6 -6
  136. package/esm/server/services/token.d.ts +15 -8
  137. package/esm/server/services/token.js +1 -1
  138. package/esm/server/signing-keys.d.ts +24 -9
  139. package/esm/server/signing-keys.js +20 -9
  140. package/esm/testing/contract/mod.d.ts +3 -3
  141. package/esm/testing/contract/mod.js +3 -3
  142. package/esm/testing/contract/tenant.d.ts +1 -1
  143. package/esm/testing/contract/tenant.js +2 -2
  144. package/esm/testing/mod.d.ts +6 -4
  145. package/esm/testing/mod.js +6 -4
  146. package/esm/testing/services.d.ts +1 -1
  147. package/esm/testing/services.js +1 -1
  148. package/esm/testing/tenant.d.ts +14 -5
  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
@@ -40,8 +40,9 @@ export class RefreshTokenGrant extends AbstractGrant {
40
40
  * authorization.
41
41
  *
42
42
  * @throws {InvalidRequestError} If `refresh_token` is missing.
43
- * @throws {InvalidGrantError} If the refresh token is unknown or expired, or
44
- * its family has outlived the absolute cap.
43
+ * @throws {InvalidGrantError} If the refresh token is unknown or expired, its
44
+ * family has outlived the absolute cap, or a concurrent exchange already
45
+ * claimed it.
45
46
  * @throws {InvalidClientError} If the token was issued to another client.
46
47
  * @throws {InvalidScopeError} If the requested scope exceeds the original grant.
47
48
  */
@@ -98,7 +99,7 @@ export class RefreshTokenGrant extends AbstractGrant {
98
99
  if (familyExpiresAt && familyExpiresAt.getTime() <= Date.now()) {
99
100
  throw new InvalidGrantError("refresh_token family expired");
100
101
  }
101
- const nextToken = await this.generateToken(client, user, nextScope, tokenService, currentToken.authenticationContext);
102
+ const nextToken = await this.generateToken(client, user, nextScope, tokenService, currentToken.authenticationContext, familyExpiresAt);
102
103
  const nextRefreshToken = "refreshToken" in nextToken
103
104
  ? nextToken
104
105
  : {
@@ -46,10 +46,13 @@ export interface IntrospectionTokenReaderOptions<Client extends ClientInterface,
46
46
  * `undefined` when the token doesn't represent a user. Omit entirely if
47
47
  * your app never reads user info off the token.
48
48
  *
49
- * A machine token (client credentials) carries no `sub`, so `data.sub` is
50
- * present only when there is a resource owner — `data.sub ? {…} : undefined`
51
- * is the whole discriminator, and a client-credentials caller resolves to no
52
- * user.
49
+ * This package's authorization server sets `sub` from its `subjectOf`
50
+ * option (the user's `id` by default — the same value its id_token and
51
+ * UserInfo carry) and omits it for a token with no user, whatever its
52
+ * `introspectionClaims` hook returns, so `data.sub` is present only when
53
+ * there is a resource owner and `data.sub ? {…} : undefined` is the whole
54
+ * discriminator. Another authorization server may set `sub` to the client
55
+ * id for a machine token; check how yours answers.
53
56
  *
54
57
  * May be async — return a promise to enrich from a DB lookup or the
55
58
  * authorization server's OIDC UserInfo endpoint, using `data.sub` as
@@ -81,6 +84,11 @@ export interface IntrospectionTokenReaderOptions<Client extends ClientInterface,
81
84
  * types via the {@linkcode IntrospectionTokenReaderOptions.getClient} and
82
85
  * {@linkcode IntrospectionTokenReaderOptions.getUser} mappers.
83
86
  *
87
+ * The reader does not check `aud` or `iss`: any token the endpoint reports
88
+ * active with a bearer `token_type` is accepted. When one authorization server
89
+ * issues tokens for several APIs, check the audience yourself (it is on
90
+ * {@linkcode Token.claims}) or have the endpoint scope its answers.
91
+ *
84
92
  * @example
85
93
  * ```ts
86
94
  * import {
@@ -137,9 +145,10 @@ export declare class IntrospectionTokenReader<Client extends ClientInterface, Us
137
145
  * returns a 5xx — a down authorization server must be distinguishable from
138
146
  * a genuinely invalid token (which is `undefined` → `invalid_token`), not
139
147
  * collapsed into it.
140
- * @throws {ServerError} If the endpoint returns a 4xx (e.g. the reader's own
141
- * introspection credentials are rejected) — a misconfiguration, not a
142
- * verdict on the caller's token.
148
+ * @throws {ServerError} If the endpoint returns any other non-2xx status
149
+ * (e.g. the reader's own introspection credentials are rejected) or a body
150
+ * that is not JSON — a misconfiguration, not a verdict on the caller's
151
+ * token.
143
152
  *
144
153
  * An active response is only accepted when its `token_type` names a bearer
145
154
  * token. RFC 7662 answers for refresh tokens too, and a refresh token's
@@ -22,6 +22,11 @@ import { encodeBasicAuth } from "../utils/basic-auth.js";
22
22
  * types via the {@linkcode IntrospectionTokenReaderOptions.getClient} and
23
23
  * {@linkcode IntrospectionTokenReaderOptions.getUser} mappers.
24
24
  *
25
+ * The reader does not check `aud` or `iss`: any token the endpoint reports
26
+ * active with a bearer `token_type` is accepted. When one authorization server
27
+ * issues tokens for several APIs, check the audience yourself (it is on
28
+ * {@linkcode Token.claims}) or have the endpoint scope its answers.
29
+ *
25
30
  * @example
26
31
  * ```ts
27
32
  * import {
@@ -87,9 +92,10 @@ export class IntrospectionTokenReader {
87
92
  * returns a 5xx — a down authorization server must be distinguishable from
88
93
  * a genuinely invalid token (which is `undefined` → `invalid_token`), not
89
94
  * collapsed into it.
90
- * @throws {ServerError} If the endpoint returns a 4xx (e.g. the reader's own
91
- * introspection credentials are rejected) — a misconfiguration, not a
92
- * verdict on the caller's token.
95
+ * @throws {ServerError} If the endpoint returns any other non-2xx status
96
+ * (e.g. the reader's own introspection credentials are rejected) or a body
97
+ * that is not JSON — a misconfiguration, not a verdict on the caller's
98
+ * token.
93
99
  *
94
100
  * An active response is only accepted when its `token_type` names a bearer
95
101
  * token. RFC 7662 answers for refresh tokens too, and a refresh token's
@@ -255,9 +255,9 @@ export declare class JwksTokenReader<Client extends ClientInterface, User, S ext
255
255
  * available — a down issuer must be distinguishable from a genuinely
256
256
  * invalid token, not collapsed into it. Once keys are cached, a failed
257
257
  * refresh is absorbed and the cached keys keep serving.
258
- * @throws {ServerError} If the JWKS endpoint returns a 4xx or a body that
259
- * is not a JWKS, or discovery reports no `jwks_uri` — a
260
- * misconfiguration, not a verdict on the caller's token.
258
+ * @throws {ServerError} If, with no keys cached, the JWKS endpoint returns
259
+ * a 4xx or a body that is not a JWKS, or discovery reports no `jwks_uri`
260
+ * — a misconfiguration, not a verdict on the caller's token.
261
261
  */
262
262
  getToken(accessToken: string): Promise<Token<Client, User, S> | undefined>;
263
263
  }
@@ -276,9 +276,9 @@ export class JwksTokenReader {
276
276
  * available — a down issuer must be distinguishable from a genuinely
277
277
  * invalid token, not collapsed into it. Once keys are cached, a failed
278
278
  * refresh is absorbed and the cached keys keep serving.
279
- * @throws {ServerError} If the JWKS endpoint returns a 4xx or a body that
280
- * is not a JWKS, or discovery reports no `jwks_uri` — a
281
- * misconfiguration, not a verdict on the caller's token.
279
+ * @throws {ServerError} If, with no keys cached, the JWKS endpoint returns
280
+ * a 4xx or a body that is not a JWKS, or discovery reports no `jwks_uri`
281
+ * — a misconfiguration, not a verdict on the caller's token.
282
282
  */
283
283
  async getToken(accessToken) {
284
284
  const parsed = parseJws(accessToken);
@@ -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,5 +1,5 @@
1
1
  /**
2
- * Contract test suite for a Udibo tenant's authorization answers: what
2
+ * Contract test suite for a Udibo Identity tenant's authorization answers: what
3
3
  * introspection reports about a signed-in person, what `GET /api/memberships`
4
4
  * lists for them, and what `POST /api/check` and `POST /api/check/batch`
5
5
  * answer.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Contract test suite for a Udibo tenant's authorization answers: what
2
+ * Contract test suite for a Udibo Identity tenant's authorization answers: what
3
3
  * introspection reports about a signed-in person, what `GET /api/memberships`
4
4
  * lists for them, and what `POST /api/check` and `POST /api/check/batch`
5
5
  * answer.
@@ -32,7 +32,7 @@ import { encodeBasicAuth } from "../../utils/basic-auth.js";
32
32
  * safe as long as nothing else uses that namespace.
33
33
  */
34
34
  export function runTenantContractTests(options) {
35
- describe(options.describeName ?? "Udibo tenant contract", () => {
35
+ describe(options.describeName ?? "Udibo Identity tenant contract", () => {
36
36
  let tenant;
37
37
  let metadata;
38
38
  let send;
@@ -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,14 +20,16 @@
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
  *
27
- * - **A fake Udibo tenant.** {@link createFakeTenant} answers a tenant's
29
+ * - **A fake Udibo Identity tenant.** {@link createFakeTenant} answers a tenant's
28
30
  * protocol surface and its `/api/check` questions over `fetch`, with the
29
31
  * claims a real tenant issues — permissions, the organization picked at
30
- * sign-in, and resource grants — so an app built on a Udibo tenant can test
32
+ * sign-in, and resource grants — so an app built on a Udibo Identity tenant can test
31
33
  * its own routes without running the identity service.
32
34
  *
33
35
  * Nothing in this module is an OAuth2 primitive — only testing support.
@@ -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,14 +20,16 @@
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
  *
27
- * - **A fake Udibo tenant.** {@link createFakeTenant} answers a tenant's
29
+ * - **A fake Udibo Identity tenant.** {@link createFakeTenant} answers a tenant's
28
30
  * protocol surface and its `/api/check` questions over `fetch`, with the
29
31
  * claims a real tenant issues — permissions, the organization picked at
30
- * sign-in, and resource grants — so an app built on a Udibo tenant can test
32
+ * sign-in, and resource grants — so an app built on a Udibo Identity tenant can test
31
33
  * its own routes without running the identity service.
32
34
  *
33
35
  * Nothing in this module is an OAuth2 primitive — only testing support.
@@ -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