@udibo/oauth2 0.4.1 → 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 (159) 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/mod.d.ts +4 -2
  143. package/esm/testing/mod.js +4 -2
  144. package/esm/testing/services.d.ts +1 -1
  145. package/esm/testing/services.js +1 -1
  146. package/esm/testing/tenant.d.ts +13 -4
  147. package/esm/utils/_buffer.d.ts +3 -2
  148. package/esm/utils/_buffer.js +3 -2
  149. package/esm/utils/_timing-safe.d.ts +8 -6
  150. package/esm/utils/_timing-safe.js +8 -6
  151. package/esm/utils/basic-auth.d.ts +5 -12
  152. package/esm/utils/basic-auth.js +5 -12
  153. package/esm/utils/crypto.d.ts +2 -1
  154. package/esm/utils/crypto.js +2 -1
  155. package/esm/utils/text.d.ts +2 -9
  156. package/esm/utils/text.js +2 -9
  157. package/esm/utils/url.d.ts +2 -2
  158. package/esm/utils/url.js +0 -2
  159. package/package.json +1 -1
@@ -67,7 +67,8 @@ async function importPrivateKey(providerId, pem) {
67
67
  *
68
68
  * @throws {ExternalAuthError} `configuration` naming the offending input when
69
69
  * the team id / key id / client id is empty, the `.p8` is not a valid P-256
70
- * key, or the requested TTL exceeds Apple's cap.
70
+ * key, or the requested TTL is not a finite number between 1 and Apple's
71
+ * cap (`NaN` and `Infinity` are rejected, never signed into `exp`).
71
72
  *
72
73
  * @example
73
74
  * ```ts
@@ -86,7 +87,8 @@ export async function generateAppleClientSecret(options) {
86
87
  const clientId = requireNonEmpty(providerId, options.clientId, "client id (Services ID)");
87
88
  const ttl = options.expiresInSeconds ??
88
89
  DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS;
89
- if (ttl <= 0 || ttl > APPLE_CLIENT_SECRET_MAX_TTL_SECONDS) {
90
+ if (!Number.isFinite(ttl) || ttl <= 0 ||
91
+ ttl > APPLE_CLIENT_SECRET_MAX_TTL_SECONDS) {
90
92
  throw configError(providerId, `Apple client secret TTL must be between 1 and ` +
91
93
  `${APPLE_CLIENT_SECRET_MAX_TTL_SECONDS} seconds (6 months); ` +
92
94
  `got ${ttl}.`);
@@ -111,10 +113,11 @@ function encodeSegment(value) {
111
113
  /**
112
114
  * Build a self-renewing Apple client-secret source. It signs a secret with
113
115
  * {@link generateAppleClientSecret} on first call, caches it, and re-signs
114
- * automatically once the cached secret is within `renewBeforeSeconds` of expiry
115
- * — so a long-lived {@link appleProvider} never presents an expired assertion.
116
- * The key material is validated on the first call, surfacing a misconfiguration
117
- * as a `configuration` {@link ExternalAuthError}.
116
+ * automatically once the cached secret is within `renewBeforeSeconds` (default
117
+ * one hour) of expiry — so a long-lived {@link appleProvider} never presents
118
+ * an expired assertion. The key material is validated on the first call, not
119
+ * at construction, surfacing a misconfiguration as a `configuration`
120
+ * {@link ExternalAuthError}; a failed call caches nothing.
118
121
  */
119
122
  export function createAppleClientSecretFactory(options) {
120
123
  const ttl = options.expiresInSeconds ??
@@ -3,9 +3,9 @@
3
3
  * {@link oidcProvider}: its client secret is a signed ES256 JWT rather than a
4
4
  * static string (see {@link generateAppleClientSecret}), it authenticates the
5
5
  * client in the token body (`client_secret_post`), and it requires
6
- * `response_mode=form_post` whenever profile scopes are requested. This is a
7
- * dedicated {@link ExternalProvider} that exchanges the code, then verifies the
8
- * `id_token` signature against Apple's JWKS.
6
+ * `response_mode=form_post` when the `name` or `email` scope is requested.
7
+ * This is a dedicated {@link ExternalProvider} that exchanges the code, then
8
+ * verifies the `id_token` signature against Apple's JWKS.
9
9
  *
10
10
  * The user's name is returned by Apple **only** on the first authorization, in
11
11
  * the `form_post` body's `user` field — never in the `id_token`. This connector
@@ -33,20 +33,27 @@ export interface AppleProviderOptions extends Pick<AppleClientSecretOptions, "te
33
33
  * `.p8` key material. Defaults to a cached factory over the key material.
34
34
  */
35
35
  clientSecret?: AppleClientSecretFactory;
36
- /** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
36
+ /**
37
+ * Fetch implementation for all provider traffic. Defaults to
38
+ * `globalThis.fetch`. A wrapper must pass `init` through intact: its
39
+ * `redirect` and `signal` carry the connector's redirect refusal and timeout.
40
+ */
37
41
  fetch?: typeof fetch;
38
42
  }
39
43
  /**
40
44
  * Creates a Sign in with Apple connector with id `"apple"`.
41
45
  *
42
- * The flow requests `response_mode=form_post` (Apple's requirement when `name`
43
- * or `email` scope is present), exchanges the code at Apple's token endpoint
44
- * with a freshly-signed client-secret JWT, and verifies the returned
46
+ * The flow requests `response_mode=form_post` whenever any scope is requested
47
+ * (Apple requires it for `name` or `email`), exchanges the code at Apple's
48
+ * token endpoint with the client-secret JWT from the factory (by default
49
+ * cached and re-signed an hour before it expires), and verifies the returned
45
50
  * `id_token`'s RS256 signature against Apple's published keys before trusting
46
51
  * its claims (`iss`, `aud`, `azp`, `exp`, and the `nonce` when one was sent,
47
- * checked exactly as {@link oidcProvider} checks them). Apple's
48
- * private-relay addresses (`@privaterelay.appleid.com`) are ordinary verified
49
- * emails and are returned as-is. The raw `id_token` claims — including
52
+ * checked as {@link oidcProvider} checks them, with the default `azp` policy).
53
+ * `emailVerified` is `true` when Apple's `email_verified` claim is `true` or
54
+ * the string `"true"`; private-relay addresses
55
+ * (`@privaterelay.appleid.com`) are returned as-is. The raw `id_token`
56
+ * claims — including
50
57
  * `is_private_email` — stay on {@link ExternalProfile.raw}.
51
58
  *
52
59
  * @example
@@ -3,9 +3,9 @@
3
3
  * {@link oidcProvider}: its client secret is a signed ES256 JWT rather than a
4
4
  * static string (see {@link generateAppleClientSecret}), it authenticates the
5
5
  * client in the token body (`client_secret_post`), and it requires
6
- * `response_mode=form_post` whenever profile scopes are requested. This is a
7
- * dedicated {@link ExternalProvider} that exchanges the code, then verifies the
8
- * `id_token` signature against Apple's JWKS.
6
+ * `response_mode=form_post` when the `name` or `email` scope is requested.
7
+ * This is a dedicated {@link ExternalProvider} that exchanges the code, then
8
+ * verifies the `id_token` signature against Apple's JWKS.
9
9
  *
10
10
  * The user's name is returned by Apple **only** on the first authorization, in
11
11
  * the `form_post` body's `user` field — never in the `id_token`. This connector
@@ -33,14 +33,17 @@ const SIGNATURE_NOT_VERIFIED = `Apple id_token signature did not verify against
33
33
  /**
34
34
  * Creates a Sign in with Apple connector with id `"apple"`.
35
35
  *
36
- * The flow requests `response_mode=form_post` (Apple's requirement when `name`
37
- * or `email` scope is present), exchanges the code at Apple's token endpoint
38
- * with a freshly-signed client-secret JWT, and verifies the returned
36
+ * The flow requests `response_mode=form_post` whenever any scope is requested
37
+ * (Apple requires it for `name` or `email`), exchanges the code at Apple's
38
+ * token endpoint with the client-secret JWT from the factory (by default
39
+ * cached and re-signed an hour before it expires), and verifies the returned
39
40
  * `id_token`'s RS256 signature against Apple's published keys before trusting
40
41
  * its claims (`iss`, `aud`, `azp`, `exp`, and the `nonce` when one was sent,
41
- * checked exactly as {@link oidcProvider} checks them). Apple's
42
- * private-relay addresses (`@privaterelay.appleid.com`) are ordinary verified
43
- * emails and are returned as-is. The raw `id_token` claims — including
42
+ * checked as {@link oidcProvider} checks them, with the default `azp` policy).
43
+ * `emailVerified` is `true` when Apple's `email_verified` claim is `true` or
44
+ * the string `"true"`; private-relay addresses
45
+ * (`@privaterelay.appleid.com`) are returned as-is. The raw `id_token`
46
+ * claims — including
44
47
  * `is_private_email` — stay on {@link ExternalProfile.raw}.
45
48
  *
46
49
  * @example
@@ -12,7 +12,11 @@ export interface DiscordProviderOptions {
12
12
  clientSecret: string;
13
13
  /** Scopes requested by default. Defaults to `["identify", "email"]`. */
14
14
  scopes?: string[];
15
- /** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
15
+ /**
16
+ * Fetch implementation for all provider traffic. Defaults to
17
+ * `globalThis.fetch`. A wrapper must pass `init` through intact: its
18
+ * `redirect` and `signal` carry the connector's redirect refusal and timeout.
19
+ */
16
20
  fetch?: typeof fetch;
17
21
  }
18
22
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The error contract for external-provider (social login) flows.
3
3
  *
4
- * Every failure in the external connector layer is thrown as an
4
+ * Failures the external connectors detect are thrown as an
5
5
  * {@link ExternalAuthError} whose message names the provider, what failed, and
6
6
  * the likely fix — misconfiguration is meant to be diagnosable from the
7
7
  * message alone. The {@link ExternalAuthErrorCode} tells app code how to
@@ -44,5 +44,9 @@ export declare class ExternalAuthError extends Error {
44
44
  /** Builds the error; `message` is prefixed with `[provider]` automatically. */
45
45
  constructor(provider: string, code: ExternalAuthErrorCode, message: string, options?: ErrorOptions);
46
46
  }
47
- /** Type guard for {@link ExternalAuthError}. */
47
+ /**
48
+ * Type guard for {@link ExternalAuthError}. Uses `instanceof`, so an error from
49
+ * another realm or a second copy of the package fails it — check
50
+ * `name === "ExternalAuthError"` there.
51
+ */
48
52
  export declare function isExternalAuthError(value: unknown): value is ExternalAuthError;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The error contract for external-provider (social login) flows.
3
3
  *
4
- * Every failure in the external connector layer is thrown as an
4
+ * Failures the external connectors detect are thrown as an
5
5
  * {@link ExternalAuthError} whose message names the provider, what failed, and
6
6
  * the likely fix — misconfiguration is meant to be diagnosable from the
7
7
  * message alone. The {@link ExternalAuthErrorCode} tells app code how to
@@ -30,7 +30,11 @@ export class ExternalAuthError extends Error {
30
30
  this.code = code;
31
31
  }
32
32
  }
33
- /** Type guard for {@link ExternalAuthError}. */
33
+ /**
34
+ * Type guard for {@link ExternalAuthError}. Uses `instanceof`, so an error from
35
+ * another realm or a second copy of the package fails it — check
36
+ * `name === "ExternalAuthError"` there.
37
+ */
34
38
  export function isExternalAuthError(value) {
35
39
  return value instanceof ExternalAuthError;
36
40
  }
@@ -36,7 +36,9 @@ export interface ExternalAuthStartResult {
36
36
  url: string;
37
37
  /**
38
38
  * Per-attempt state to persist in a cookie/session and hand back to
39
- * {@link ExternalAuthFlow.finish}. JSON-serializable; contains no tokens.
39
+ * {@link ExternalAuthFlow.finish}. JSON-serializable and contains no tokens,
40
+ * but the PKCE verifier and nonce are secrets — keep it sealed or
41
+ * server-side.
40
42
  */
41
43
  transient: ExternalAuthTransient;
42
44
  }
@@ -94,11 +96,12 @@ export declare class ExternalAuthFlow {
94
96
  */
95
97
  start(input: ExternalAuthStartInput): Promise<ExternalAuthStartResult>;
96
98
  /**
97
- * Completes a sign-in attempt from the provider callback: enforces the max
98
- * transient age, surfaces provider `error` callback params, verifies
99
- * `state` with a timing-safe compare, exchanges the code, and (for OIDC
100
- * providers) validates the id_token nonce — then returns the normalized
101
- * profile.
99
+ * Completes a sign-in attempt from the provider callback: checks the
100
+ * transient belongs to this flow's provider, enforces the max transient
101
+ * age, verifies `state` with a timing-safe compare, and only then surfaces a
102
+ * provider `error` callback param; finally the provider exchanges the code
103
+ * and (for nonce-using providers) validates the id_token nonce, and the
104
+ * normalized profile is returned.
102
105
  *
103
106
  * @throws {ExternalAuthError} with a code describing the failure — see
104
107
  * {@link ExternalAuthErrorCode} for how app code should respond to each.
@@ -88,11 +88,12 @@ export class ExternalAuthFlow {
88
88
  };
89
89
  }
90
90
  /**
91
- * Completes a sign-in attempt from the provider callback: enforces the max
92
- * transient age, surfaces provider `error` callback params, verifies
93
- * `state` with a timing-safe compare, exchanges the code, and (for OIDC
94
- * providers) validates the id_token nonce — then returns the normalized
95
- * profile.
91
+ * Completes a sign-in attempt from the provider callback: checks the
92
+ * transient belongs to this flow's provider, enforces the max transient
93
+ * age, verifies `state` with a timing-safe compare, and only then surfaces a
94
+ * provider `error` callback param; finally the provider exchanges the code
95
+ * and (for nonce-using providers) validates the id_token nonce, and the
96
+ * normalized profile is returned.
96
97
  *
97
98
  * @throws {ExternalAuthError} with a code describing the failure — see
98
99
  * {@link ExternalAuthErrorCode} for how app code should respond to each.
@@ -13,14 +13,18 @@ export interface GithubProviderOptions {
13
13
  clientSecret: string;
14
14
  /** Scopes requested by default. Defaults to `["read:user", "user:email"]`. */
15
15
  scopes?: string[];
16
- /** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
16
+ /**
17
+ * Fetch implementation for all provider traffic. Defaults to
18
+ * `globalThis.fetch`. A wrapper must pass `init` through intact: its
19
+ * `redirect` and `signal` carry the connector's redirect refusal and timeout.
20
+ */
17
21
  fetch?: typeof fetch;
18
22
  }
19
23
  /**
20
24
  * Creates a GitHub connector with id `"github"`.
21
25
  *
22
- * GitHub speaks plain OAuth2 (no discovery, no id_token, no PKCE for OAuth
23
- * Apps), so the connector uses GitHub's fixed endpoints and builds the
26
+ * GitHub speaks plain OAuth2 (no discovery, no id_token), so the connector
27
+ * uses GitHub's fixed endpoints, sends no PKCE challenge, and builds the
24
28
  * profile from `GET /user` plus `GET /user/emails`: the primary email is used
25
29
  * (falling back to the first verified, then the first listed), and
26
30
  * `emailVerified` reflects GitHub's flag for whichever email was chosen. When
@@ -14,8 +14,8 @@ const GITHUB_EMAILS_URL = "https://api.github.com/user/emails";
14
14
  /**
15
15
  * Creates a GitHub connector with id `"github"`.
16
16
  *
17
- * GitHub speaks plain OAuth2 (no discovery, no id_token, no PKCE for OAuth
18
- * Apps), so the connector uses GitHub's fixed endpoints and builds the
17
+ * GitHub speaks plain OAuth2 (no discovery, no id_token), so the connector
18
+ * uses GitHub's fixed endpoints, sends no PKCE challenge, and builds the
19
19
  * profile from `GET /user` plus `GET /user/emails`: the primary email is used
20
20
  * (falling back to the first verified, then the first listed), and
21
21
  * `emailVerified` reflects GitHub's flag for whichever email was chosen. When
@@ -21,7 +21,11 @@ export interface GoogleProviderOptions {
21
21
  * `aud` is this one. See {@link AzpPolicy}.
22
22
  */
23
23
  azp?: AzpPolicy;
24
- /** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
24
+ /**
25
+ * Fetch implementation for all provider traffic. Defaults to
26
+ * `globalThis.fetch`. A wrapper must pass `init` through intact: its
27
+ * `redirect` and `signal` carry the connector's redirect refusal and timeout.
28
+ */
25
29
  fetch?: typeof fetch;
26
30
  /**
27
31
  * Cache Google's discovery document is read from and written to, shared with
@@ -76,9 +76,20 @@ export interface OAuth2ProviderOptions {
76
76
  scopeSeparator?: string;
77
77
  /** Whether to run PKCE (S256). Defaults to `false`. */
78
78
  usesPkce?: boolean;
79
- /** Extra fixed query params to add to the authorize URL. */
79
+ /**
80
+ * Extra fixed query params to add to the authorize URL (an `audience`, a
81
+ * tenant hint). The protocol params always win: `response_type`,
82
+ * `client_id`, `redirect_uri`, `scope`, `state`, the PKCE `code_challenge`
83
+ * pair, and a `prompt` the flow passes are written after these, so a
84
+ * same-named key here cannot replace the connector's CSRF `state` or
85
+ * downgrade PKCE.
86
+ */
80
87
  authorizationParams?: Record<string, string>;
81
- /** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
88
+ /**
89
+ * Fetch implementation for all provider traffic. Defaults to
90
+ * `globalThis.fetch`. A wrapper must pass `init` through intact: its
91
+ * `redirect` and `signal` carry the connector's redirect refusal and timeout.
92
+ */
82
93
  fetch?: typeof fetch;
83
94
  }
84
95
  /**
@@ -100,14 +100,14 @@ export function oauth2Provider(options) {
100
100
  usesNonce: false,
101
101
  async buildAuthorizationUrl(input) {
102
102
  const url = new URL(options.authorizationEndpoint);
103
+ for (const [key, value] of Object.entries(options.authorizationParams ?? {})) {
104
+ url.searchParams.set(key, value);
105
+ }
103
106
  url.searchParams.set("response_type", "code");
104
107
  url.searchParams.set("client_id", options.clientId);
105
108
  url.searchParams.set("redirect_uri", input.redirectUri);
106
109
  url.searchParams.set("scope", input.scopes.join(scopeSeparator));
107
110
  url.searchParams.set("state", input.state);
108
- for (const [key, value] of Object.entries(options.authorizationParams ?? {})) {
109
- url.searchParams.set(key, value);
110
- }
111
111
  if (input.codeVerifier) {
112
112
  url.searchParams.set("code_challenge", await generateCodeChallenge(input.codeVerifier));
113
113
  url.searchParams.set("code_challenge_method", "S256");
@@ -14,9 +14,11 @@ export interface OidcProviderOptions {
14
14
  /** Human-readable name for buttons and logs. Defaults to `"OpenID Connect"`. */
15
15
  displayName?: string;
16
16
  /**
17
- * The provider's issuer URL. Endpoints are resolved from
18
- * `${issuer}/.well-known/openid-configuration` (with an RFC 8414 fallback)
19
- * on first use and cached.
17
+ * The provider's issuer URL. Endpoints are resolved on first use from
18
+ * `${issuer}/.well-known/oauth-authorization-server` (RFC 8414), falling
19
+ * back to `${issuer}/.well-known/openid-configuration`, and cached. Must be
20
+ * `https` (plain `http` only for `localhost`, `127.0.0.1`, or `[::1]`), as
21
+ * must every endpoint the discovery document advertises.
20
22
  */
21
23
  issuer: string;
22
24
  /** The client id registered with the provider. */
@@ -36,7 +38,11 @@ export interface OidcProviderOptions {
36
38
  * see {@link AzpPolicy}.
37
39
  */
38
40
  azp?: AzpPolicy;
39
- /** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
41
+ /**
42
+ * Fetch implementation for all provider traffic. Defaults to
43
+ * `globalThis.fetch`. A wrapper must pass `init` through intact: its
44
+ * `redirect` and `signal` carry the connector's redirect refusal and timeout.
45
+ */
40
46
  fetch?: typeof fetch;
41
47
  /**
42
48
  * Cache the discovery document is read from and written to, shared with any
@@ -53,18 +59,23 @@ export interface OidcProviderOptions {
53
59
  * The exchange reuses the package's `DirectClient` (discovery caching with
54
60
  * both well-known paths, Basic client auth, RFC 6749 error mapping) with PKCE
55
61
  * always on. The profile is built from the id_token claims, merged with the
56
- * UserInfo response when the provider advertises a `userinfo_endpoint` (the
57
- * UserInfo `sub` must match the id_token `sub` per OIDC Core §5.3.2, and a
58
- * UserInfo failure is non-fatal — the id_token claims already suffice).
62
+ * UserInfo response when the provider advertises a `userinfo_endpoint`;
63
+ * UserInfo values override id_token claims of the same name (including
64
+ * `email_verified`). A UserInfo response whose `sub` differs from the
65
+ * id_token `sub` (OIDC Core §5.3.2) is discarded, and a UserInfo failure is
66
+ * non-fatal — the profile then comes from the id_token claims alone.
59
67
  *
60
68
  * **id_token validation:** the `iss`, `aud`, `azp` (when the token has multiple
61
69
  * audiences or an `azp` claim, unless `azp: "ignore"` is configured), `exp`,
62
70
  * and `nonce` claims are checked per OpenID Connect Core §3.1.3.7. The
63
71
  * signature is deliberately **not** verified: the token arrives directly from
64
72
  * the token endpoint over TLS, where §3.1.3.7 permits relying on TLS server
65
- * validation instead of signature checks (rule 6). Every other rule —
66
- * including expiry (rule 9) — still applies. Do not feed this connector
67
- * id_tokens obtained out-of-band.
73
+ * validation instead of signature checks (rule 6). Only the claim checks
74
+ * listed above are made — not `iat`, `acr`, or `auth_time`. Do not feed this
75
+ * connector id_tokens obtained out-of-band.
76
+ *
77
+ * @throws {ExternalAuthError} `configuration` when `issuer` is not an absolute
78
+ * `https` URL (see {@link OidcProviderOptions.issuer}).
68
79
  *
69
80
  * @example
70
81
  * ```ts
@@ -16,18 +16,23 @@ import { ExternalAuthError } from "./errors.js";
16
16
  * The exchange reuses the package's `DirectClient` (discovery caching with
17
17
  * both well-known paths, Basic client auth, RFC 6749 error mapping) with PKCE
18
18
  * always on. The profile is built from the id_token claims, merged with the
19
- * UserInfo response when the provider advertises a `userinfo_endpoint` (the
20
- * UserInfo `sub` must match the id_token `sub` per OIDC Core §5.3.2, and a
21
- * UserInfo failure is non-fatal — the id_token claims already suffice).
19
+ * UserInfo response when the provider advertises a `userinfo_endpoint`;
20
+ * UserInfo values override id_token claims of the same name (including
21
+ * `email_verified`). A UserInfo response whose `sub` differs from the
22
+ * id_token `sub` (OIDC Core §5.3.2) is discarded, and a UserInfo failure is
23
+ * non-fatal — the profile then comes from the id_token claims alone.
22
24
  *
23
25
  * **id_token validation:** the `iss`, `aud`, `azp` (when the token has multiple
24
26
  * audiences or an `azp` claim, unless `azp: "ignore"` is configured), `exp`,
25
27
  * and `nonce` claims are checked per OpenID Connect Core §3.1.3.7. The
26
28
  * signature is deliberately **not** verified: the token arrives directly from
27
29
  * the token endpoint over TLS, where §3.1.3.7 permits relying on TLS server
28
- * validation instead of signature checks (rule 6). Every other rule —
29
- * including expiry (rule 9) — still applies. Do not feed this connector
30
- * id_tokens obtained out-of-band.
30
+ * validation instead of signature checks (rule 6). Only the claim checks
31
+ * listed above are made — not `iat`, `acr`, or `auth_time`. Do not feed this
32
+ * connector id_tokens obtained out-of-band.
33
+ *
34
+ * @throws {ExternalAuthError} `configuration` when `issuer` is not an absolute
35
+ * `https` URL (see {@link OidcProviderOptions.issuer}).
31
36
  *
32
37
  * @example
33
38
  * ```ts
@@ -27,7 +27,8 @@ export interface ExternalProfile {
27
27
  email?: string;
28
28
  /**
29
29
  * `true` only when the provider positively asserted the email is verified
30
- * (OIDC `email_verified: true`, GitHub verified primary email). Defaults to
30
+ * (OIDC `email_verified: true`, GitHub's `verified` flag on the email the
31
+ * connector chose). Defaults to
31
32
  * `false` — treat unverified emails as attacker-controllable input and do
32
33
  * not auto-link accounts on them.
33
34
  */
@@ -100,7 +101,8 @@ export interface ExternalProfileInput {
100
101
  * verification, transient expiry, callback error surfacing); the provider
101
102
  * owns the wire protocol (authorize URL shape, code exchange, id_token nonce
102
103
  * validation, profile normalization). Implement this only for providers the
103
- * built-in {@link oidcProvider} / {@link githubProvider} cannot cover.
104
+ * built-in connectors, including the generic {@link oidcProvider} and
105
+ * {@link oauth2Provider}, cannot cover.
104
106
  */
105
107
  export interface ExternalProvider {
106
108
  /** Stable id used in {@link ExternalProfile.provider} and error messages. */
@@ -13,8 +13,8 @@ export type IdentifierKind = "email" | "username" | "phone";
13
13
  * Classify a sign-in identifier as an email, phone number, or username.
14
14
  *
15
15
  * Heuristic: an `@` → `email`; otherwise a value made only of phone characters
16
- * (`+`, digits, spaces, `-`, `()`, `.`) with at least 7 digits → `phone`;
17
- * everything else → `username`. The value is trimmed first.
16
+ * (an optional leading `+`, then digits, spaces, `-`, `()`, `.`) with at least
17
+ * 7 digits → `phone`; everything else → `username`. The value is trimmed first.
18
18
  */
19
19
  export declare function classifyIdentifier(identifier: string): IdentifierKind;
20
20
  /** Per-kind user lookups supplied by the app. Omit a kind to disallow it. */
@@ -28,9 +28,10 @@ export interface IdentifierLookups<User> {
28
28
  }
29
29
  /**
30
30
  * Build a resolver that classifies an identifier ({@link classifyIdentifier})
31
- * and dispatches to the matching app-supplied lookup. Returns `undefined` when
32
- * no lookup is configured for the classified kind, or the user isn't found —
33
- * callers should treat both as "invalid credentials" without revealing which.
31
+ * and dispatches the trimmed value to the matching app-supplied lookup. Returns
32
+ * `undefined` when no lookup is configured for the classified kind, or the
33
+ * user isn't found — callers should treat both as "invalid credentials"
34
+ * without revealing which.
34
35
  *
35
36
  * @example
36
37
  * ```ts
@@ -11,8 +11,8 @@
11
11
  * Classify a sign-in identifier as an email, phone number, or username.
12
12
  *
13
13
  * Heuristic: an `@` → `email`; otherwise a value made only of phone characters
14
- * (`+`, digits, spaces, `-`, `()`, `.`) with at least 7 digits → `phone`;
15
- * everything else → `username`. The value is trimmed first.
14
+ * (an optional leading `+`, then digits, spaces, `-`, `()`, `.`) with at least
15
+ * 7 digits → `phone`; everything else → `username`. The value is trimmed first.
16
16
  */
17
17
  export function classifyIdentifier(identifier) {
18
18
  const value = identifier.trim();
@@ -25,9 +25,10 @@ export function classifyIdentifier(identifier) {
25
25
  }
26
26
  /**
27
27
  * Build a resolver that classifies an identifier ({@link classifyIdentifier})
28
- * and dispatches to the matching app-supplied lookup. Returns `undefined` when
29
- * no lookup is configured for the classified kind, or the user isn't found —
30
- * callers should treat both as "invalid credentials" without revealing which.
28
+ * and dispatches the trimmed value to the matching app-supplied lookup. Returns
29
+ * `undefined` when no lookup is configured for the classified kind, or the
30
+ * user isn't found — callers should treat both as "invalid credentials"
31
+ * without revealing which.
31
32
  *
32
33
  * @example
33
34
  * ```ts
@@ -9,7 +9,7 @@
9
9
  * database to share state across instances — e.g. columns on your users table,
10
10
  * which also gives admins visibility).
11
11
  *
12
- * `AccountLockout` is a pure state machine: it never decides whether to block.
12
+ * `AccountLockout` only records state: it never decides whether to block.
13
13
  * `IdentityService` consults it and applies its `protectionMode`
14
14
  * (enforce vs log-only), so adopters can observe lockout behavior before
15
15
  * turning it on.
@@ -124,8 +124,9 @@ export interface AccountLockoutLike {
124
124
  /**
125
125
  * Consecutive-failure lockout with safe defaults (10 failures → 15-minute
126
126
  * lock) — the default {@link AccountLockoutLike}. Wire into
127
- * {@link IdentityService} via its `lockout` option; call {@link reset} from
128
- * your unlock flow.
127
+ * {@link IdentityService} via its `lockout` option, which resets it on a
128
+ * successful sign-in, password reset, or {@link IdentityService.unlockAccount};
129
+ * call {@link reset} yourself only from an unlock flow of your own.
129
130
  */
130
131
  export declare class AccountLockout implements AccountLockoutLike {
131
132
  #private;
@@ -9,7 +9,7 @@
9
9
  * database to share state across instances — e.g. columns on your users table,
10
10
  * which also gives admins visibility).
11
11
  *
12
- * `AccountLockout` is a pure state machine: it never decides whether to block.
12
+ * `AccountLockout` only records state: it never decides whether to block.
13
13
  * `IdentityService` consults it and applies its `protectionMode`
14
14
  * (enforce vs log-only), so adopters can observe lockout behavior before
15
15
  * turning it on.
@@ -21,8 +21,9 @@ const DEFAULT_LOCK_DURATION_MS = 15 * 60 * 1000;
21
21
  /**
22
22
  * Consecutive-failure lockout with safe defaults (10 failures → 15-minute
23
23
  * lock) — the default {@link AccountLockoutLike}. Wire into
24
- * {@link IdentityService} via its `lockout` option; call {@link reset} from
25
- * your unlock flow.
24
+ * {@link IdentityService} via its `lockout` option, which resets it on a
25
+ * successful sign-in, password reset, or {@link IdentityService.unlockAccount};
26
+ * call {@link reset} yourself only from an unlock flow of your own.
26
27
  */
27
28
  export class AccountLockout {
28
29
  #maxAttempts;
@@ -12,7 +12,8 @@
12
12
  * {@link verifyRecoveryCode} — readable single-use fallback codes, hashed at
13
13
  * rest.
14
14
  * - {@link MfaService} over an app-implemented {@link MfaStore} — enrollment
15
- * (pending → confirmed), verification with atomic replay and burn handling,
15
+ * (pending → confirmed), verification with replay rejection and
16
+ * recovery-code burning through the store's atomic methods,
16
17
  * regeneration, disable, plus the layer's usual seams (`rateLimiter`,
17
18
  * `onEvent` for `mfa.*` audit events). {@link MemoryMfaStore} ships for
18
19
  * dev/tests.
@@ -12,7 +12,8 @@
12
12
  * {@link verifyRecoveryCode} — readable single-use fallback codes, hashed at
13
13
  * rest.
14
14
  * - {@link MfaService} over an app-implemented {@link MfaStore} — enrollment
15
- * (pending → confirmed), verification with atomic replay and burn handling,
15
+ * (pending → confirmed), verification with replay rejection and
16
+ * recovery-code burning through the store's atomic methods,
16
17
  * regeneration, disable, plus the layer's usual seams (`rateLimiter`,
17
18
  * `onEvent` for `mfa.*` audit events). {@link MemoryMfaStore} ships for
18
19
  * dev/tests.
@@ -83,7 +83,8 @@ export type RecoveryCodeVerification = {
83
83
  * The index of the matching hash. Burn it with an atomic delete-by-value
84
84
  * (a conditional `DELETE` on the hash, never read-filter-write) so the
85
85
  * code cannot be redeemed again — the library does not do this for you at
86
- * this level ({@link MfaService} does, atomically).
86
+ * this level ({@link MfaService} does, through its store's
87
+ * `consumeRecoveryHash`).
87
88
  */
88
89
  index: number;
89
90
  };