better-auth 1.6.3 → 1.7.0-beta.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 (45) hide show
  1. package/dist/api/index.d.mts +4 -0
  2. package/dist/api/routes/callback.d.mts +2 -0
  3. package/dist/api/routes/callback.mjs +22 -13
  4. package/dist/client/path-to-object.d.mts +35 -1
  5. package/dist/client/plugins/index.d.mts +3 -2
  6. package/dist/client/plugins/index.mjs +2 -1
  7. package/dist/oauth2/error-codes.d.mts +20 -0
  8. package/dist/oauth2/error-codes.mjs +20 -0
  9. package/dist/package.mjs +1 -1
  10. package/dist/plugins/admin/admin.mjs +1 -1
  11. package/dist/plugins/anonymous/index.mjs +1 -1
  12. package/dist/plugins/generic-oauth/client.d.mts +6 -6
  13. package/dist/plugins/generic-oauth/client.mjs +6 -0
  14. package/dist/plugins/generic-oauth/error-codes.d.mts +1 -6
  15. package/dist/plugins/generic-oauth/error-codes.mjs +2 -7
  16. package/dist/plugins/generic-oauth/index.d.mts +9 -156
  17. package/dist/plugins/generic-oauth/index.mjs +133 -73
  18. package/dist/plugins/generic-oauth/providers/auth0.d.mts +1 -1
  19. package/dist/plugins/generic-oauth/providers/gumroad.d.mts +1 -1
  20. package/dist/plugins/generic-oauth/providers/hubspot.d.mts +1 -1
  21. package/dist/plugins/generic-oauth/providers/keycloak.d.mts +1 -1
  22. package/dist/plugins/generic-oauth/providers/microsoft-entra-id.d.mts +1 -1
  23. package/dist/plugins/generic-oauth/providers/okta.d.mts +1 -1
  24. package/dist/plugins/generic-oauth/providers/patreon.d.mts +1 -1
  25. package/dist/plugins/generic-oauth/providers/slack.d.mts +1 -1
  26. package/dist/plugins/generic-oauth/types.d.mts +25 -27
  27. package/dist/plugins/index.d.mts +3 -3
  28. package/dist/plugins/index.mjs +2 -2
  29. package/dist/plugins/jwt/client.d.mts +1 -1
  30. package/dist/plugins/jwt/index.d.mts +3 -3
  31. package/dist/plugins/jwt/index.mjs +2 -2
  32. package/dist/plugins/jwt/sign.d.mts +15 -3
  33. package/dist/plugins/jwt/sign.mjs +31 -12
  34. package/dist/plugins/jwt/types.d.mts +13 -1
  35. package/dist/plugins/jwt/utils.d.mts +1 -1
  36. package/dist/plugins/last-login-method/index.mjs +1 -1
  37. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  38. package/dist/plugins/two-factor/client.d.mts +2 -0
  39. package/dist/plugins/two-factor/error-code.d.mts +2 -0
  40. package/dist/plugins/two-factor/error-code.mjs +2 -0
  41. package/dist/plugins/two-factor/index.d.mts +19 -0
  42. package/dist/plugins/two-factor/index.mjs +48 -25
  43. package/dist/test-utils/test-instance.d.mts +12 -0
  44. package/package.json +8 -8
  45. package/dist/plugins/generic-oauth/routes.mjs +0 -407
@@ -1,6 +1,5 @@
1
1
  import { PACKAGE_VERSION } from "../../version.mjs";
2
2
  import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes.mjs";
3
- import { getUserInfo, oAuth2Callback, oAuth2LinkAccount, signInWithOAuth2 } from "./routes.mjs";
4
3
  import { auth0 } from "./providers/auth0.mjs";
5
4
  import { gumroad } from "./providers/gumroad.mjs";
6
5
  import { hubspot } from "./providers/hubspot.mjs";
@@ -12,10 +11,61 @@ import { patreon } from "./providers/patreon.mjs";
12
11
  import { slack } from "./providers/slack.mjs";
13
12
  import { APIError } from "@better-auth/core/error";
14
13
  import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode } from "@better-auth/core/oauth2";
14
+ import { decodeJwt } from "jose";
15
15
  import { betterFetch } from "@better-fetch/fetch";
16
16
  //#region src/plugins/generic-oauth/index.ts
17
+ function buildClientAssertion(config, tokenEndpoint) {
18
+ if (config.authentication !== "private_key_jwt" || !config.clientAssertion) return;
19
+ return {
20
+ ...config.clientAssertion,
21
+ tokenEndpoint
22
+ };
23
+ }
24
+ async function fetchDiscovery(url, headers) {
25
+ const result = await betterFetch(url, {
26
+ method: "GET",
27
+ headers
28
+ });
29
+ if (result.error || !result.data) return null;
30
+ if (result.data.issuer) try {
31
+ new URL(result.data.issuer);
32
+ } catch {
33
+ return null;
34
+ }
35
+ return result.data;
36
+ }
37
+ async function fetchUserInfo(tokens, userInfoUrl) {
38
+ if (tokens.idToken) try {
39
+ const decoded = decodeJwt(tokens.idToken);
40
+ if (decoded?.sub && decoded?.email) return {
41
+ id: decoded.sub,
42
+ emailVerified: decoded.email_verified,
43
+ image: decoded.picture,
44
+ ...decoded
45
+ };
46
+ } catch {}
47
+ if (!userInfoUrl) return null;
48
+ const userInfo = await betterFetch(userInfoUrl, {
49
+ method: "GET",
50
+ headers: { Authorization: `Bearer ${tokens.accessToken}` }
51
+ });
52
+ if (userInfo.error || !userInfo.data) return null;
53
+ const data = userInfo.data;
54
+ return {
55
+ ...data,
56
+ id: data.sub ?? data.id ?? "",
57
+ emailVerified: data.email_verified ?? false,
58
+ email: data.email,
59
+ image: data.picture,
60
+ name: data.name
61
+ };
62
+ }
17
63
  /**
18
- * A generic OAuth plugin that can be used to add OAuth support to any provider
64
+ * A generic OAuth plugin that registers any OAuth/OIDC provider
65
+ * as a first-class social provider.
66
+ *
67
+ * Providers are used through the standard `signIn.social` and
68
+ * `callback/:id` core endpoints — no plugin-specific endpoints needed.
19
69
  */
20
70
  const genericOAuth = (options) => {
21
71
  const seenIds = /* @__PURE__ */ new Set();
@@ -29,25 +79,34 @@ const genericOAuth = (options) => {
29
79
  return {
30
80
  id: "generic-oauth",
31
81
  version: PACKAGE_VERSION,
32
- init: (ctx) => {
33
- return { context: { socialProviders: options.config.map((c) => {
34
- let finalUserInfoUrl = c.userInfoUrl;
35
- return {
82
+ init: async (ctx) => {
83
+ const genericProviders = [];
84
+ for (const c of options.config) {
85
+ let authorizationUrl = c.authorizationUrl;
86
+ let tokenUrl = c.tokenUrl;
87
+ let userInfoUrl = c.userInfoUrl;
88
+ let issuer;
89
+ let isOidc = false;
90
+ if (c.discoveryUrl) {
91
+ const discovered = await fetchDiscovery(c.discoveryUrl, c.discoveryHeaders).catch((err) => {
92
+ ctx.logger.error(`Discovery fetch failed for "${c.providerId}": ${err}`);
93
+ return null;
94
+ });
95
+ if (discovered) {
96
+ authorizationUrl ??= discovered.authorization_endpoint;
97
+ tokenUrl ??= discovered.token_endpoint;
98
+ userInfoUrl ??= discovered.userinfo_endpoint;
99
+ issuer = discovered.issuer;
100
+ isOidc = Array.isArray(discovered.id_token_signing_alg_values_supported) && discovered.id_token_signing_alg_values_supported.length > 0;
101
+ } else if (!authorizationUrl || !tokenUrl) ctx.logger.error(`Provider "${c.providerId}": discovery returned no data and no explicit endpoints configured. OAuth sign-in will fail for this provider.`);
102
+ }
103
+ if (!c.clientSecret && !c.clientAssertion && c.authentication !== "private_key_jwt") ctx.logger.warn(`Provider "${c.providerId}": no clientSecret or clientAssertion configured. Token exchange will fail unless this is a public client.`);
104
+ genericProviders.push({
36
105
  id: c.providerId,
37
- name: c.providerId,
38
- async createAuthorizationURL(data) {
39
- let finalAuthUrl = c.authorizationUrl;
40
- if (!finalAuthUrl && c.discoveryUrl) {
41
- const discovery = await betterFetch(c.discoveryUrl, {
42
- method: "GET",
43
- headers: c.discoveryHeaders
44
- });
45
- if (discovery.data) {
46
- finalAuthUrl = discovery.data.authorization_endpoint;
47
- finalUserInfoUrl = finalUserInfoUrl ?? discovery.data.userinfo_endpoint;
48
- }
49
- }
50
- if (!finalAuthUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
106
+ name: c.name ?? c.providerId,
107
+ issuer,
108
+ createAuthorizationURL(data) {
109
+ if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
51
110
  return createAuthorizationURL({
52
111
  id: c.providerId,
53
112
  options: {
@@ -55,51 +114,64 @@ const genericOAuth = (options) => {
55
114
  clientSecret: c.clientSecret,
56
115
  redirectURI: c.redirectURI
57
116
  },
58
- authorizationEndpoint: finalAuthUrl,
117
+ authorizationEndpoint: authorizationUrl,
59
118
  state: data.state,
60
- codeVerifier: c.pkce ? data.codeVerifier : void 0,
61
- scopes: c.scopes || [],
62
- redirectURI: `${ctx.baseURL}/oauth2/callback/${c.providerId}`
119
+ codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
120
+ scopes: (() => {
121
+ const merged = [...data.scopes ?? [], ...c.scopes ?? []];
122
+ if (isOidc && !merged.includes("openid")) merged.unshift("openid");
123
+ return merged;
124
+ })(),
125
+ redirectURI: data.redirectURI,
126
+ prompt: c.prompt,
127
+ accessType: c.accessType,
128
+ responseType: c.responseType,
129
+ responseMode: c.responseMode,
130
+ additionalParams: c.authorizationUrlParams,
131
+ loginHint: data.loginHint
63
132
  });
64
133
  },
65
134
  async validateAuthorizationCode(data) {
66
135
  if (c.getToken) return c.getToken(data);
67
- let finalTokenUrl = c.tokenUrl;
68
- if (c.discoveryUrl) {
69
- const discovery = await betterFetch(c.discoveryUrl, {
70
- method: "GET",
71
- headers: c.discoveryHeaders
72
- });
73
- if (discovery.data) {
74
- finalTokenUrl = discovery.data.token_endpoint;
75
- finalUserInfoUrl = discovery.data.userinfo_endpoint;
76
- }
77
- }
78
- if (!finalTokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
136
+ if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
79
137
  return validateAuthorizationCode({
80
138
  headers: c.authorizationHeaders,
81
139
  code: data.code,
82
- codeVerifier: data.codeVerifier,
140
+ codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
83
141
  redirectURI: data.redirectURI,
84
142
  options: {
85
143
  clientId: c.clientId,
86
144
  clientSecret: c.clientSecret,
87
145
  redirectURI: c.redirectURI
88
146
  },
89
- tokenEndpoint: finalTokenUrl,
90
- authentication: c.authentication
147
+ tokenEndpoint: tokenUrl,
148
+ authentication: c.authentication,
149
+ additionalParams: c.tokenUrlParams,
150
+ clientAssertion: buildClientAssertion(c, tokenUrl)
91
151
  });
92
152
  },
153
+ async getUserInfo(tokens) {
154
+ const raw = c.getUserInfo ? await c.getUserInfo(tokens) : await fetchUserInfo(tokens, userInfoUrl);
155
+ if (!raw) return null;
156
+ const mapped = c.mapProfileToUser ? await c.mapProfileToUser(raw) : {};
157
+ const user = {
158
+ id: raw.id,
159
+ email: raw.email,
160
+ emailVerified: raw.emailVerified,
161
+ image: raw.image,
162
+ name: raw.name,
163
+ ...mapped
164
+ };
165
+ return {
166
+ user: {
167
+ ...user,
168
+ image: user.image ?? void 0
169
+ },
170
+ data: raw
171
+ };
172
+ },
93
173
  async refreshAccessToken(refreshToken) {
94
- let finalTokenUrl = c.tokenUrl;
95
- if (c.discoveryUrl) {
96
- const discovery = await betterFetch(c.discoveryUrl, {
97
- method: "GET",
98
- headers: c.discoveryHeaders
99
- });
100
- if (discovery.data) finalTokenUrl = discovery.data.token_endpoint;
101
- }
102
- if (!finalTokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
174
+ if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
103
175
  return refreshAccessToken({
104
176
  refreshToken,
105
177
  options: {
@@ -107,33 +179,21 @@ const genericOAuth = (options) => {
107
179
  clientSecret: c.clientSecret
108
180
  },
109
181
  authentication: c.authentication,
110
- tokenEndpoint: finalTokenUrl
182
+ clientAssertion: buildClientAssertion(c, tokenUrl),
183
+ tokenEndpoint: tokenUrl
111
184
  });
112
185
  },
113
- async getUserInfo(tokens) {
114
- const userInfo = c.getUserInfo ? await c.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
115
- if (!userInfo) return null;
116
- const userMap = await c.mapProfileToUser?.(userInfo);
117
- return {
118
- user: {
119
- id: userInfo?.id,
120
- email: userInfo?.email,
121
- emailVerified: userInfo?.emailVerified,
122
- image: userInfo?.image,
123
- name: userInfo?.name,
124
- ...userMap
125
- },
126
- data: userInfo
127
- };
128
- },
129
- options: { overrideUserInfoOnSignIn: c.overrideUserInfo }
130
- };
131
- }).concat(ctx.socialProviders) } };
132
- },
133
- endpoints: {
134
- signInWithOAuth2: signInWithOAuth2(options),
135
- oAuth2Callback: oAuth2Callback(options),
136
- oAuth2LinkAccount: oAuth2LinkAccount(options)
186
+ disableImplicitSignUp: c.disableImplicitSignUp,
187
+ disableSignUp: c.disableSignUp,
188
+ options: {
189
+ disableSignUp: c.disableSignUp,
190
+ overrideUserInfoOnSignIn: c.overrideUserInfo
191
+ }
192
+ });
193
+ }
194
+ const existingIds = new Set(ctx.socialProviders.map((p) => p.id));
195
+ for (const gp of genericProviders) if (existingIds.has(gp.id)) ctx.logger.warn(`Generic OAuth provider "${gp.id}" shadows a built-in social provider with the same ID`);
196
+ return { context: { socialProviders: genericProviders.concat(ctx.socialProviders) } };
137
197
  },
138
198
  options,
139
199
  $ERROR_CODES: GENERIC_OAUTH_ERROR_CODES
@@ -31,6 +31,6 @@ interface Auth0Options extends BaseOAuthProviderOptions {
31
31
  * });
32
32
  * ```
33
33
  */
34
- declare function auth0(options: Auth0Options): GenericOAuthConfig;
34
+ declare function auth0(options: Auth0Options): GenericOAuthConfig<"auth0">;
35
35
  //#endregion
36
36
  export { Auth0Options, auth0 };
@@ -26,6 +26,6 @@ interface GumroadOptions extends BaseOAuthProviderOptions {}
26
26
  *
27
27
  * @see https://app.gumroad.com/oauth
28
28
  */
29
- declare function gumroad(options: GumroadOptions): GenericOAuthConfig;
29
+ declare function gumroad(options: GumroadOptions): GenericOAuthConfig<"gumroad">;
30
30
  //#endregion
31
31
  export { GumroadOptions, gumroad };
@@ -31,6 +31,6 @@ interface HubSpotOptions extends BaseOAuthProviderOptions {
31
31
  * });
32
32
  * ```
33
33
  */
34
- declare function hubspot(options: HubSpotOptions): GenericOAuthConfig;
34
+ declare function hubspot(options: HubSpotOptions): GenericOAuthConfig<"hubspot">;
35
35
  //#endregion
36
36
  export { HubSpotOptions, hubspot };
@@ -31,6 +31,6 @@ interface KeycloakOptions extends BaseOAuthProviderOptions {
31
31
  * });
32
32
  * ```
33
33
  */
34
- declare function keycloak(options: KeycloakOptions): GenericOAuthConfig;
34
+ declare function keycloak(options: KeycloakOptions): GenericOAuthConfig<"keycloak">;
35
35
  //#endregion
36
36
  export { KeycloakOptions, keycloak };
@@ -31,6 +31,6 @@ interface MicrosoftEntraIdOptions extends BaseOAuthProviderOptions {
31
31
  * });
32
32
  * ```
33
33
  */
34
- declare function microsoftEntraId(options: MicrosoftEntraIdOptions): GenericOAuthConfig;
34
+ declare function microsoftEntraId(options: MicrosoftEntraIdOptions): GenericOAuthConfig<"microsoft-entra-id">;
35
35
  //#endregion
36
36
  export { MicrosoftEntraIdOptions, microsoftEntraId };
@@ -31,6 +31,6 @@ interface OktaOptions extends BaseOAuthProviderOptions {
31
31
  * });
32
32
  * ```
33
33
  */
34
- declare function okta(options: OktaOptions): GenericOAuthConfig;
34
+ declare function okta(options: OktaOptions): GenericOAuthConfig<"okta">;
35
35
  //#endregion
36
36
  export { OktaOptions, okta };
@@ -24,6 +24,6 @@ interface PatreonOptions extends BaseOAuthProviderOptions {}
24
24
  * });
25
25
  * ```
26
26
  */
27
- declare function patreon(options: PatreonOptions): GenericOAuthConfig;
27
+ declare function patreon(options: PatreonOptions): GenericOAuthConfig<"patreon">;
28
28
  //#endregion
29
29
  export { PatreonOptions, patreon };
@@ -24,6 +24,6 @@ interface SlackOptions extends BaseOAuthProviderOptions {}
24
24
  * });
25
25
  * ```
26
26
  */
27
- declare function slack(options: SlackOptions): GenericOAuthConfig;
27
+ declare function slack(options: SlackOptions): GenericOAuthConfig<"slack">;
28
28
  //#endregion
29
29
  export { SlackOptions, slack };
@@ -1,39 +1,29 @@
1
- import { GenericEndpointContext } from "@better-auth/core";
2
1
  import { User } from "@better-auth/core/db";
3
- import { OAuth2Tokens, OAuth2UserInfo } from "@better-auth/core/oauth2";
2
+ import { ClientAssertionConfig, OAuth2Tokens, OAuth2UserInfo } from "@better-auth/core/oauth2";
4
3
 
5
4
  //#region src/plugins/generic-oauth/types.d.ts
6
- interface GenericOAuthOptions {
5
+ interface GenericOAuthOptions<ID extends string = string> {
7
6
  /**
8
7
  * Array of OAuth provider configurations.
9
8
  */
10
- config: GenericOAuthConfig[];
9
+ config: GenericOAuthConfig<ID>[];
11
10
  }
12
11
  /**
13
12
  * Configuration interface for generic OAuth providers.
14
13
  */
15
- interface GenericOAuthConfig {
14
+ interface GenericOAuthConfig<ID extends string = string> {
16
15
  /** Unique identifier for the OAuth provider */
17
- providerId: string;
16
+ providerId: ID;
17
+ /**
18
+ * Human-readable display name for this provider.
19
+ * Defaults to `providerId` if not set.
20
+ */
21
+ name?: string | undefined;
18
22
  /**
19
23
  * URL to fetch OAuth 2.0 configuration.
20
24
  * If provided, the authorization and token endpoints will be fetched from this URL.
21
25
  */
22
26
  discoveryUrl?: string | undefined;
23
- /**
24
- * The expected issuer identifier for validation.
25
- * If not provided but discoveryUrl is set, it will be fetched from the discovery document.
26
- * When set, the callback validates that the `iss` parameter matches this value.
27
- * @see https://datatracker.ietf.org/doc/html/rfc9207
28
- */
29
- issuer?: string | undefined;
30
- /**
31
- * When true, requires the `iss` parameter in callbacks if an issuer is configured.
32
- * This provides stricter security but may break with older OAuth servers
33
- * that don't support issuer identification.
34
- * @default false
35
- */
36
- requireIssuerValidation?: boolean | undefined;
37
27
  /**
38
28
  * URL for the authorization endpoint.
39
29
  * Optional if using discoveryUrl.
@@ -78,8 +68,10 @@ interface GenericOAuthConfig {
78
68
  */
79
69
  prompt?: ("none" | "login" | "create" | "consent" | "select_account" | "select_account consent" | "login consent") | undefined;
80
70
  /**
81
- * Whether to use PKCE (Proof Key for Code Exchange)
82
- * @default false
71
+ * Whether to use PKCE (Proof Key for Code Exchange).
72
+ * Required by OAuth 2.1 for all authorization code flows.
73
+ * Disable only for providers that explicitly reject PKCE.
74
+ * @default true
83
75
  */
84
76
  pkce?: boolean | undefined;
85
77
  /**
@@ -108,19 +100,20 @@ interface GenericOAuthConfig {
108
100
  */
109
101
  getUserInfo?: ((tokens: OAuth2Tokens) => Promise<OAuth2UserInfo | null>) | undefined;
110
102
  /**
111
- * Custom function to map the user profile to a User object.
103
+ * Custom function to map the provider's user profile to your app's user fields.
104
+ * The profile contains standard OAuth2 fields plus any provider-specific extras.
112
105
  */
113
- mapProfileToUser?: ((profile: Record<string, any>) => Partial<Partial<User>> | Promise<Partial<User>>) | undefined;
106
+ mapProfileToUser?: ((profile: OAuth2UserInfo & Record<string, unknown>) => Partial<User> | Promise<Partial<User>>) | undefined;
114
107
  /**
115
108
  * Additional search-params to add to the authorizationUrl.
116
109
  * Warning: Search-params added here overwrite any default params.
117
110
  */
118
- authorizationUrlParams?: (Record<string, string> | ((ctx: GenericEndpointContext) => Record<string, string>)) | undefined;
111
+ authorizationUrlParams?: Record<string, string> | undefined;
119
112
  /**
120
113
  * Additional search-params to add to the tokenUrl.
121
114
  * Warning: Search-params added here overwrite any default params.
122
115
  */
123
- tokenUrlParams?: (Record<string, string> | ((ctx: GenericEndpointContext) => Record<string, string>)) | undefined;
116
+ tokenUrlParams?: Record<string, string> | undefined;
124
117
  /**
125
118
  * Disable implicit sign up for new users. When set to true for the provider,
126
119
  * sign-in need to be called with with requestSignUp as true to create new users.
@@ -134,7 +127,12 @@ interface GenericOAuthConfig {
134
127
  * Authentication method for token requests.
135
128
  * @default "post"
136
129
  */
137
- authentication?: ("basic" | "post") | undefined;
130
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
131
+ /**
132
+ * Client assertion config for `private_key_jwt` authentication.
133
+ * Required when `authentication` is `"private_key_jwt"`.
134
+ */
135
+ clientAssertion?: ClientAssertionConfig | undefined;
138
136
  /**
139
137
  * Custom headers to include in the discovery request.
140
138
  * Useful for providers like Epic that require specific headers (e.g., Epic-Client-ID).
@@ -29,8 +29,8 @@ import { PatreonOptions, patreon } from "./generic-oauth/providers/patreon.mjs";
29
29
  import { SlackOptions, slack } from "./generic-oauth/providers/slack.mjs";
30
30
  import { BaseOAuthProviderOptions, genericOAuth } from "./generic-oauth/index.mjs";
31
31
  import { HaveIBeenPwnedOptions, haveIBeenPwned } from "./haveibeenpwned/index.mjs";
32
- import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "./jwt/types.mjs";
33
- import { getJwtToken, signJWT } from "./jwt/sign.mjs";
32
+ import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey } from "./jwt/types.mjs";
33
+ import { getJwtToken, resolveSigningKey, signJWT } from "./jwt/sign.mjs";
34
34
  import { createJwk, generateExportedKeyPair, toExpJWT } from "./jwt/utils.mjs";
35
35
  import { verifyJWT } from "./jwt/verify.mjs";
36
36
  import { jwt } from "./jwt/index.mjs";
@@ -62,4 +62,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
62
62
  import { UsernameOptions, username } from "./username/index.mjs";
63
63
  import { hasPermission } from "./organization/has-permission.mjs";
64
64
  import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
65
- export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
65
+ export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
@@ -23,7 +23,7 @@ import { slack } from "./generic-oauth/providers/slack.mjs";
23
23
  import { genericOAuth } from "./generic-oauth/index.mjs";
24
24
  import { haveIBeenPwned } from "./haveibeenpwned/index.mjs";
25
25
  import { createJwk, generateExportedKeyPair, toExpJWT } from "./jwt/utils.mjs";
26
- import { getJwtToken, signJWT } from "./jwt/sign.mjs";
26
+ import { getJwtToken, resolveSigningKey, signJWT } from "./jwt/sign.mjs";
27
27
  import { verifyJWT } from "./jwt/verify.mjs";
28
28
  import { jwt } from "./jwt/index.mjs";
29
29
  import { lastLoginMethod } from "./last-login-method/index.mjs";
@@ -43,4 +43,4 @@ import { siwe } from "./siwe/index.mjs";
43
43
  import { testUtils } from "./test-utils/index.mjs";
44
44
  import { twoFactor } from "./two-factor/index.mjs";
45
45
  import { username } from "./username/index.mjs";
46
- export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
46
+ export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, resolveSigningKey, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
@@ -1,4 +1,4 @@
1
- import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "./types.mjs";
1
+ import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey } from "./types.mjs";
2
2
  import { jwt } from "./index.mjs";
3
3
  import { JSONWebKeySet } from "jose";
4
4
  import * as _better_fetch_fetch0 from "@better-fetch/fetch";
@@ -1,5 +1,5 @@
1
- import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "./types.mjs";
2
- import { getJwtToken, signJWT } from "./sign.mjs";
1
+ import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey } from "./types.mjs";
2
+ import { getJwtToken, resolveSigningKey, signJWT } from "./sign.mjs";
3
3
  import { createJwk, generateExportedKeyPair, toExpJWT } from "./utils.mjs";
4
4
  import { verifyJWT } from "./verify.mjs";
5
5
  import * as _better_auth_core0 from "@better-auth/core";
@@ -221,4 +221,4 @@ declare const jwt: <O extends JwtOptions>(options?: O) => {
221
221
  };
222
222
  };
223
223
  //#endregion
224
- export { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, createJwk, generateExportedKeyPair, getJwtToken, jwt, signJWT, toExpJWT, verifyJWT };
224
+ export { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey, createJwk, generateExportedKeyPair, getJwtToken, jwt, resolveSigningKey, signJWT, toExpJWT, verifyJWT };
@@ -5,7 +5,7 @@ import { PACKAGE_VERSION } from "../../version.mjs";
5
5
  import { getJwksAdapter } from "./adapter.mjs";
6
6
  import { schema } from "./schema.mjs";
7
7
  import { createJwk, generateExportedKeyPair, toExpJWT } from "./utils.mjs";
8
- import { getJwtToken, signJWT } from "./sign.mjs";
8
+ import { getJwtToken, resolveSigningKey, signJWT } from "./sign.mjs";
9
9
  import { verifyJWT } from "./verify.mjs";
10
10
  import { BetterAuthError } from "@better-auth/core/error";
11
11
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
@@ -198,4 +198,4 @@ const jwt = (options) => {
198
198
  };
199
199
  };
200
200
  //#endregion
201
- export { createJwk, generateExportedKeyPair, getJwtToken, jwt, signJWT, toExpJWT, verifyJWT };
201
+ export { createJwk, generateExportedKeyPair, getJwtToken, jwt, resolveSigningKey, signJWT, toExpJWT, verifyJWT };
@@ -1,4 +1,4 @@
1
- import { JwtOptions } from "./types.mjs";
1
+ import { JwtOptions, ResolvedSigningKey } from "./types.mjs";
2
2
  import { GenericEndpointContext } from "@better-auth/core";
3
3
 
4
4
  //#region src/plugins/jwt/sign.d.ts
@@ -47,10 +47,22 @@ type JWTPayloadWithOptional = {
47
47
  iat?: number | undefined; /** Any other JWT Claim Set member. */
48
48
  [propName: string]: unknown | undefined;
49
49
  };
50
+ /**
51
+ * Resolves the JWKS signing key, decrypts it, and imports it
52
+ * for use with jose's SignJWT. Returns null when signing is
53
+ * delegated to a custom jwt.sign callback.
54
+ *
55
+ * Callers that need the signing algorithm before constructing
56
+ * the JWT payload (e.g. for OIDC at_hash) should call this
57
+ * first, read `.alg`, then pass the result to `signJWT` via
58
+ * the `resolvedKey` option to avoid a redundant DB lookup.
59
+ */
60
+ declare function resolveSigningKey(ctx: GenericEndpointContext, options?: JwtOptions): Promise<ResolvedSigningKey | null>;
50
61
  declare function signJWT(ctx: GenericEndpointContext, config: {
51
62
  options?: JwtOptions | undefined;
52
- payload: JWTPayloadWithOptional;
63
+ payload: JWTPayloadWithOptional; /** Pre-resolved key from resolveSigningKey. Skips redundant DB lookup. */
64
+ resolvedKey?: ResolvedSigningKey;
53
65
  }): Promise<string>;
54
66
  declare function getJwtToken(ctx: GenericEndpointContext, options?: JwtOptions | undefined): Promise<string>;
55
67
  //#endregion
56
- export { getJwtToken, signJWT };
68
+ export { getJwtToken, resolveSigningKey, signJWT };
@@ -4,6 +4,34 @@ import { createJwk, toExpJWT } from "./utils.mjs";
4
4
  import { BetterAuthError } from "@better-auth/core/error";
5
5
  import { SignJWT, importJWK } from "jose";
6
6
  //#region src/plugins/jwt/sign.ts
7
+ /**
8
+ * Resolves the JWKS signing key, decrypts it, and imports it
9
+ * for use with jose's SignJWT. Returns null when signing is
10
+ * delegated to a custom jwt.sign callback.
11
+ *
12
+ * Callers that need the signing algorithm before constructing
13
+ * the JWT payload (e.g. for OIDC at_hash) should call this
14
+ * first, read `.alg`, then pass the result to `signJWT` via
15
+ * the `resolvedKey` option to avoid a redundant DB lookup.
16
+ */
17
+ async function resolveSigningKey(ctx, options) {
18
+ if (options?.jwt?.sign) return null;
19
+ let key = await getJwksAdapter(ctx.context.adapter, options).getLatestKey(ctx);
20
+ if (!key || key.expiresAt && key.expiresAt < /* @__PURE__ */ new Date()) key = await createJwk(ctx, options);
21
+ const privateWebKey = !options?.jwks?.disablePrivateKeyEncryption ? await symmetricDecrypt({
22
+ key: ctx.context.secretConfig,
23
+ data: JSON.parse(key.privateKey)
24
+ }).catch(() => {
25
+ throw new BetterAuthError("Failed to decrypt private key. Make sure the secret currently in use is the same as the one used to encrypt the private key. If you are using a different secret, either clean up your JWKS or disable private key encryption.");
26
+ }) : key.privateKey;
27
+ const alg = key.alg ?? options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
28
+ const privateKey = await importJWK(JSON.parse(privateWebKey), alg);
29
+ return {
30
+ alg,
31
+ kid: key.id,
32
+ privateKey
33
+ };
34
+ }
7
35
  async function signJWT(ctx, config) {
8
36
  const { options } = config;
9
37
  const payload = config.payload;
@@ -29,19 +57,10 @@ async function signJWT(ctx, config) {
29
57
  };
30
58
  return options.jwt.sign(jwtPayload);
31
59
  }
32
- let key = await getJwksAdapter(ctx.context.adapter, options).getLatestKey(ctx);
33
- if (!key || key.expiresAt && key.expiresAt < /* @__PURE__ */ new Date()) key = await createJwk(ctx, options);
34
- const privateWebKey = !options?.jwks?.disablePrivateKeyEncryption ? await symmetricDecrypt({
35
- key: ctx.context.secretConfig,
36
- data: JSON.parse(key.privateKey)
37
- }).catch(() => {
38
- throw new BetterAuthError("Failed to decrypt private key. Make sure the secret currently in use is the same as the one used to encrypt the private key. If you are using a different secret, either clean up your JWKS or disable private key encryption.");
39
- }) : key.privateKey;
40
- const alg = key.alg ?? options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
41
- const privateKey = await importJWK(JSON.parse(privateWebKey), alg);
60
+ const { alg, kid, privateKey } = config.resolvedKey ?? await resolveSigningKey(ctx, options);
42
61
  const jwt = new SignJWT(payload).setProtectedHeader({
43
62
  alg,
44
- kid: key.id
63
+ kid
45
64
  }).setExpirationTime(exp).setIssuer(iss ?? defaultIss).setAudience(aud ?? defaultAud);
46
65
  if (iat) jwt.setIssuedAt(iat);
47
66
  if (payload.sub) jwt.setSubject(payload.sub);
@@ -61,4 +80,4 @@ async function getJwtToken(ctx, options) {
61
80
  });
62
81
  }
63
82
  //#endregion
64
- export { getJwtToken, signJWT };
83
+ export { getJwtToken, resolveSigningKey, signJWT };
@@ -187,5 +187,17 @@ interface Jwk {
187
187
  alg?: JWSAlgorithms | undefined;
188
188
  crv?: ("Ed25519" | "P-256" | "P-521") | undefined;
189
189
  }
190
+ /**
191
+ * A fully resolved signing key ready for JWT signing.
192
+ * Produced by `resolveSigningKey`, consumed by `signJWT`.
193
+ * Separates key resolution from signing so callers can
194
+ * read the `alg` before constructing the JWT payload
195
+ * (required for OIDC hash claims like at_hash).
196
+ */
197
+ interface ResolvedSigningKey {
198
+ alg: string;
199
+ kid: string;
200
+ privateKey: CryptoKey | Uint8Array;
201
+ }
190
202
  //#endregion
191
- export { JWKOptions, JWSAlgorithms, Jwk, JwtOptions };
203
+ export { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey };
@@ -16,7 +16,7 @@ declare function toExpJWT(expirationTime: number | Date | string, iat: number):
16
16
  declare function generateExportedKeyPair(options?: JwtOptions | undefined): Promise<{
17
17
  publicWebKey: jose.JWK;
18
18
  privateWebKey: jose.JWK;
19
- alg: "EdDSA" | "ES256" | "ES512" | "PS256" | "RS256";
19
+ alg: "RS256" | "PS256" | "ES256" | "ES512" | "EdDSA";
20
20
  cfg: {
21
21
  crv?: "Ed25519" | undefined;
22
22
  } | {