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.
- package/dist/api/index.d.mts +4 -0
- package/dist/api/routes/callback.d.mts +2 -0
- package/dist/api/routes/callback.mjs +22 -13
- package/dist/client/path-to-object.d.mts +35 -1
- package/dist/client/plugins/index.d.mts +3 -2
- package/dist/client/plugins/index.mjs +2 -1
- package/dist/oauth2/error-codes.d.mts +20 -0
- package/dist/oauth2/error-codes.mjs +20 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/admin.mjs +1 -1
- package/dist/plugins/anonymous/index.mjs +1 -1
- package/dist/plugins/generic-oauth/client.d.mts +6 -6
- package/dist/plugins/generic-oauth/client.mjs +6 -0
- package/dist/plugins/generic-oauth/error-codes.d.mts +1 -6
- package/dist/plugins/generic-oauth/error-codes.mjs +2 -7
- package/dist/plugins/generic-oauth/index.d.mts +9 -156
- package/dist/plugins/generic-oauth/index.mjs +133 -73
- package/dist/plugins/generic-oauth/providers/auth0.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/gumroad.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/hubspot.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/keycloak.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/microsoft-entra-id.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/okta.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/patreon.d.mts +1 -1
- package/dist/plugins/generic-oauth/providers/slack.d.mts +1 -1
- package/dist/plugins/generic-oauth/types.d.mts +25 -27
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/jwt/client.d.mts +1 -1
- package/dist/plugins/jwt/index.d.mts +3 -3
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/jwt/sign.d.mts +15 -3
- package/dist/plugins/jwt/sign.mjs +31 -12
- package/dist/plugins/jwt/types.d.mts +13 -1
- package/dist/plugins/jwt/utils.d.mts +1 -1
- package/dist/plugins/last-login-method/index.mjs +1 -1
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/two-factor/client.d.mts +2 -0
- package/dist/plugins/two-factor/error-code.d.mts +2 -0
- package/dist/plugins/two-factor/error-code.mjs +2 -0
- package/dist/plugins/two-factor/index.d.mts +19 -0
- package/dist/plugins/two-factor/index.mjs +48 -25
- package/dist/test-utils/test-instance.d.mts +12 -0
- package/package.json +8 -8
- 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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
if (!
|
|
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:
|
|
117
|
+
authorizationEndpoint: authorizationUrl,
|
|
59
118
|
state: data.state,
|
|
60
|
-
codeVerifier: c.pkce ? data.codeVerifier : void 0,
|
|
61
|
-
scopes:
|
|
62
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
182
|
+
clientAssertion: buildClientAssertion(c, tokenUrl),
|
|
183
|
+
tokenEndpoint: tokenUrl
|
|
111
184
|
});
|
|
112
185
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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?:
|
|
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?:
|
|
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).
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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: "
|
|
19
|
+
alg: "RS256" | "PS256" | "ES256" | "ES512" | "EdDSA";
|
|
20
20
|
cfg: {
|
|
21
21
|
crv?: "Ed25519" | undefined;
|
|
22
22
|
} | {
|