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,407 +0,0 @@
1
- import { setSessionCookie } from "../../cookies/index.mjs";
2
- import { generateState, parseState } from "../../oauth2/state.mjs";
3
- import { setTokenUtil } from "../../oauth2/utils.mjs";
4
- import { sessionMiddleware } from "../../api/routes/session.mjs";
5
- import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
6
- import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
- import { APIError as APIError$1 } from "../../api/index.mjs";
8
- import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes.mjs";
9
- import { BASE_ERROR_CODES } from "@better-auth/core/error";
10
- import { createAuthorizationURL, validateAuthorizationCode } from "@better-auth/core/oauth2";
11
- import { createAuthEndpoint } from "@better-auth/core/api";
12
- import * as z from "zod";
13
- import { decodeJwt } from "jose";
14
- import { betterFetch } from "@better-fetch/fetch";
15
- //#region src/plugins/generic-oauth/routes.ts
16
- const signInWithOAuth2BodySchema = z.object({
17
- providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }),
18
- callbackURL: z.string().meta({ description: "The URL to redirect to after sign in" }).optional(),
19
- errorCallbackURL: z.string().meta({ description: "The URL to redirect to if an error occurs" }).optional(),
20
- newUserCallbackURL: z.string().meta({ description: "The URL to redirect to after login if the user is new. Eg: \"/welcome\"" }).optional(),
21
- disableRedirect: z.boolean().meta({ description: "Disable redirect" }).optional(),
22
- scopes: z.array(z.string()).meta({ description: "Scopes to be passed to the provider authorization request." }).optional(),
23
- requestSignUp: z.boolean().meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. Eg: false" }).optional(),
24
- additionalData: z.record(z.string(), z.any()).optional()
25
- });
26
- /**
27
- * ### Endpoint
28
- *
29
- * POST `/sign-in/oauth2`
30
- *
31
- * ### API Methods
32
- *
33
- * **server:**
34
- * `auth.api.signInWithOAuth2`
35
- *
36
- * **client:**
37
- * `authClient.signIn.oauth2`
38
- *
39
- * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-oauth2)
40
- */
41
- const signInWithOAuth2 = (options) => createAuthEndpoint("/sign-in/oauth2", {
42
- method: "POST",
43
- body: signInWithOAuth2BodySchema,
44
- metadata: { openapi: {
45
- description: "Sign in with OAuth2",
46
- responses: { 200: {
47
- description: "Sign in with OAuth2",
48
- content: { "application/json": { schema: {
49
- type: "object",
50
- properties: {
51
- url: { type: "string" },
52
- redirect: { type: "boolean" }
53
- }
54
- } } }
55
- } }
56
- } }
57
- }, async (ctx) => {
58
- const { providerId } = ctx.body;
59
- const config = options.config.find((c) => c.providerId === providerId);
60
- if (!config) throw APIError$1.fromStatus("BAD_REQUEST", { message: `${GENERIC_OAUTH_ERROR_CODES.PROVIDER_CONFIG_NOT_FOUND} ${providerId}` });
61
- const { discoveryUrl, authorizationUrl, tokenUrl, clientId, clientSecret, scopes, redirectURI, responseType, pkce, prompt, accessType, authorizationUrlParams, responseMode } = config;
62
- let finalAuthUrl = authorizationUrl;
63
- let finalTokenUrl = tokenUrl;
64
- if (discoveryUrl) {
65
- const discovery = await betterFetch(discoveryUrl, {
66
- method: "GET",
67
- headers: config.discoveryHeaders,
68
- onError(context) {
69
- ctx.context.logger.error(context.error.message, context.error, { discoveryUrl });
70
- }
71
- });
72
- if (discovery.data) {
73
- finalAuthUrl = discovery.data.authorization_endpoint;
74
- finalTokenUrl = discovery.data.token_endpoint;
75
- }
76
- }
77
- if (!finalAuthUrl || !finalTokenUrl) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
78
- if (authorizationUrlParams) {
79
- const withAdditionalParams = new URL(finalAuthUrl);
80
- for (const [paramName, paramValue] of Object.entries(authorizationUrlParams)) withAdditionalParams.searchParams.set(paramName, paramValue);
81
- finalAuthUrl = withAdditionalParams.toString();
82
- }
83
- const additionalParams = typeof authorizationUrlParams === "function" ? authorizationUrlParams(ctx) : authorizationUrlParams;
84
- const { state, codeVerifier } = await generateState(ctx, void 0, ctx.body.additionalData);
85
- const authUrl = await createAuthorizationURL({
86
- id: providerId,
87
- options: {
88
- clientId,
89
- clientSecret,
90
- redirectURI
91
- },
92
- authorizationEndpoint: finalAuthUrl,
93
- state,
94
- codeVerifier: pkce ? codeVerifier : void 0,
95
- scopes: ctx.body.scopes ? [...ctx.body.scopes, ...scopes || []] : scopes || [],
96
- redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerId}`,
97
- prompt,
98
- accessType,
99
- responseType,
100
- responseMode,
101
- additionalParams
102
- });
103
- return ctx.json({
104
- url: authUrl.toString(),
105
- redirect: !ctx.body.disableRedirect
106
- });
107
- });
108
- const OAuth2CallbackQuerySchema = z.object({
109
- code: z.string().meta({ description: "The OAuth2 code" }).optional(),
110
- error: z.string().meta({ description: "The error message, if any" }).optional(),
111
- error_description: z.string().meta({ description: "The error description, if any" }).optional(),
112
- state: z.string().meta({ description: "The state parameter from the OAuth2 request" }).optional(),
113
- iss: z.string().meta({ description: "The issuer identifier" }).optional()
114
- });
115
- const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:providerId", {
116
- method: "GET",
117
- query: OAuth2CallbackQuerySchema,
118
- metadata: {
119
- ...HIDE_METADATA,
120
- allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
121
- openapi: {
122
- description: "OAuth2 callback",
123
- responses: { 200: {
124
- description: "OAuth2 callback",
125
- content: { "application/json": { schema: {
126
- type: "object",
127
- properties: { url: { type: "string" } }
128
- } } }
129
- } }
130
- }
131
- }
132
- }, async (ctx) => {
133
- const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
134
- if (ctx.query.error || !ctx.query.code) throw ctx.redirect(`${defaultErrorURL}?error=${ctx.query.error || "oAuth_code_missing"}&error_description=${ctx.query.error_description}`);
135
- const providerId = ctx.params?.providerId;
136
- if (!providerId) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.PROVIDER_ID_REQUIRED);
137
- const providerConfig = options.config.find((p) => p.providerId === providerId);
138
- if (!providerConfig) throw APIError$1.fromStatus("BAD_REQUEST", { message: `${GENERIC_OAUTH_ERROR_CODES.PROVIDER_CONFIG_NOT_FOUND} ${providerId}` });
139
- let tokens = void 0;
140
- const { callbackURL, codeVerifier, errorURL, requestSignUp, newUserURL, link } = await parseState(ctx);
141
- const code = ctx.query.code;
142
- function redirectOnError(error) {
143
- const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
144
- let url = errorURL || defaultErrorURL;
145
- if (url.includes("?")) url = `${url}&error=${error}`;
146
- else url = `${url}?error=${error}`;
147
- throw ctx.redirect(url);
148
- }
149
- let finalTokenUrl = providerConfig.tokenUrl;
150
- let finalUserInfoUrl = providerConfig.userInfoUrl;
151
- let expectedIssuer = providerConfig.issuer;
152
- if (providerConfig.discoveryUrl) {
153
- const discovery = await betterFetch(providerConfig.discoveryUrl, {
154
- method: "GET",
155
- headers: providerConfig.discoveryHeaders
156
- });
157
- if (discovery.data) {
158
- finalTokenUrl = discovery.data.token_endpoint;
159
- finalUserInfoUrl = discovery.data.userinfo_endpoint;
160
- if (!expectedIssuer && discovery.data.issuer) expectedIssuer = discovery.data.issuer;
161
- }
162
- }
163
- if (expectedIssuer) {
164
- if (ctx.query.iss) {
165
- if (ctx.query.iss !== expectedIssuer) {
166
- ctx.context.logger.error("OAuth issuer mismatch", {
167
- expected: expectedIssuer,
168
- received: ctx.query.iss
169
- });
170
- return redirectOnError("issuer_mismatch");
171
- }
172
- } else if (providerConfig.requireIssuerValidation) {
173
- ctx.context.logger.error("OAuth issuer parameter missing", { expected: expectedIssuer });
174
- return redirectOnError("issuer_missing");
175
- }
176
- }
177
- try {
178
- if (providerConfig.getToken) tokens = await providerConfig.getToken({
179
- code,
180
- redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerConfig.providerId}`,
181
- codeVerifier: providerConfig.pkce ? codeVerifier : void 0
182
- });
183
- else {
184
- if (!finalTokenUrl) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG);
185
- const additionalParams = typeof providerConfig.tokenUrlParams === "function" ? providerConfig.tokenUrlParams(ctx) : providerConfig.tokenUrlParams;
186
- tokens = await validateAuthorizationCode({
187
- headers: providerConfig.authorizationHeaders,
188
- code,
189
- codeVerifier: providerConfig.pkce ? codeVerifier : void 0,
190
- redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerConfig.providerId}`,
191
- options: {
192
- clientId: providerConfig.clientId,
193
- clientSecret: providerConfig.clientSecret,
194
- redirectURI: providerConfig.redirectURI
195
- },
196
- tokenEndpoint: finalTokenUrl,
197
- authentication: providerConfig.authentication,
198
- additionalParams
199
- });
200
- }
201
- } catch (e) {
202
- ctx.context.logger.error(e && typeof e === "object" && "name" in e ? e.name : "", e);
203
- throw redirectOnError("oauth_code_verification_failed");
204
- }
205
- if (!tokens) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG);
206
- const userInfo = await (async function handleUserInfo() {
207
- const userInfo = providerConfig.getUserInfo ? await providerConfig.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
208
- if (!userInfo) throw redirectOnError("user_info_is_missing");
209
- const mapUser = providerConfig.mapProfileToUser ? await providerConfig.mapProfileToUser(userInfo) : userInfo;
210
- const email = mapUser.email ? mapUser.email.toLowerCase() : userInfo.email?.toLowerCase();
211
- if (!email) {
212
- ctx.context.logger.error("Unable to get user info", userInfo);
213
- throw redirectOnError("email_is_missing");
214
- }
215
- const id = mapUser.id ? String(mapUser.id) : String(userInfo.id);
216
- const name = mapUser.name ? mapUser.name : userInfo.name;
217
- if (!name) {
218
- ctx.context.logger.error("Unable to get user info", userInfo);
219
- throw redirectOnError("name_is_missing");
220
- }
221
- return {
222
- ...userInfo,
223
- ...mapUser,
224
- email,
225
- id,
226
- name
227
- };
228
- })();
229
- if (link) {
230
- if (ctx.context.options.account?.accountLinking?.allowDifferentEmails !== true && link.email.toLowerCase() !== userInfo.email.toLowerCase()) return redirectOnError("email_doesn't_match");
231
- const existingAccount = await ctx.context.internalAdapter.findAccountByProviderId(String(userInfo.id), providerConfig.providerId);
232
- if (existingAccount) {
233
- if (existingAccount.userId !== link.userId) return redirectOnError("account_already_linked_to_different_user");
234
- const updateData = Object.fromEntries(Object.entries({
235
- accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
236
- idToken: tokens.idToken,
237
- refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context),
238
- accessTokenExpiresAt: tokens.accessTokenExpiresAt,
239
- refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
240
- scope: tokens.scopes?.join(",")
241
- }).filter(([_, value]) => value !== void 0));
242
- await ctx.context.internalAdapter.updateAccount(existingAccount.id, updateData);
243
- } else if (!await ctx.context.internalAdapter.createAccount({
244
- userId: link.userId,
245
- providerId: providerConfig.providerId,
246
- accountId: userInfo.id,
247
- accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
248
- accessTokenExpiresAt: tokens.accessTokenExpiresAt,
249
- refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
250
- scope: tokens.scopes?.join(","),
251
- refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context),
252
- idToken: tokens.idToken
253
- })) return redirectOnError("unable_to_link_account");
254
- let toRedirectTo;
255
- try {
256
- toRedirectTo = callbackURL.toString();
257
- } catch {
258
- toRedirectTo = callbackURL;
259
- }
260
- throw ctx.redirect(toRedirectTo);
261
- }
262
- const result = await handleOAuthUserInfo(ctx, {
263
- userInfo,
264
- account: {
265
- providerId: providerConfig.providerId,
266
- accountId: userInfo.id,
267
- ...tokens,
268
- scope: tokens.scopes?.join(",")
269
- },
270
- callbackURL,
271
- disableSignUp: providerConfig.disableImplicitSignUp && !requestSignUp || providerConfig.disableSignUp,
272
- overrideUserInfo: providerConfig.overrideUserInfo
273
- });
274
- if (result.error) return redirectOnError(result.error.split(" ").join("_"));
275
- const { session, user } = result.data;
276
- await setSessionCookie(ctx, {
277
- session,
278
- user
279
- });
280
- let toRedirectTo;
281
- try {
282
- toRedirectTo = (result.isRegister ? newUserURL || callbackURL : callbackURL).toString();
283
- } catch {
284
- toRedirectTo = result.isRegister ? newUserURL || callbackURL : callbackURL;
285
- }
286
- throw ctx.redirect(toRedirectTo);
287
- });
288
- const OAuth2LinkAccountBodySchema = z.object({
289
- providerId: z.string(),
290
- callbackURL: z.string(),
291
- scopes: z.array(z.string()).meta({ description: "Additional scopes to request when linking the account" }).optional(),
292
- errorCallbackURL: z.string().meta({ description: "The URL to redirect to if there is an error during the link process" }).optional()
293
- });
294
- /**
295
- * ### Endpoint
296
- *
297
- * POST `/oauth2/link`
298
- *
299
- * ### API Methods
300
- *
301
- * **server:**
302
- * `auth.api.oAuth2LinkAccount`
303
- *
304
- * **client:**
305
- * `authClient.oauth2.link`
306
- *
307
- * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/generic-oauth#api-method-oauth2-link)
308
- */
309
- const oAuth2LinkAccount = (options) => createAuthEndpoint("/oauth2/link", {
310
- method: "POST",
311
- body: OAuth2LinkAccountBodySchema,
312
- use: [sessionMiddleware],
313
- metadata: { openapi: {
314
- description: "Link an OAuth2 account to the current user session",
315
- responses: { "200": {
316
- description: "Authorization URL generated successfully for linking an OAuth2 account",
317
- content: { "application/json": { schema: {
318
- type: "object",
319
- properties: {
320
- url: {
321
- type: "string",
322
- format: "uri",
323
- description: "The authorization URL to redirect the user to for linking the OAuth2 account"
324
- },
325
- redirect: {
326
- type: "boolean",
327
- description: "Indicates that the client should redirect to the provided URL",
328
- enum: [true]
329
- }
330
- },
331
- required: ["url", "redirect"]
332
- } } }
333
- } }
334
- } }
335
- }, async (c) => {
336
- const session = c.context.session;
337
- if (!session) throw APIError$1.from("UNAUTHORIZED", GENERIC_OAUTH_ERROR_CODES.SESSION_REQUIRED);
338
- const provider = options.config.find((p) => p.providerId === c.body.providerId);
339
- if (!provider) throw APIError$1.from("NOT_FOUND", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
340
- const { providerId, clientId, clientSecret, redirectURI, authorizationUrl, discoveryUrl, pkce, scopes, prompt, accessType, authorizationUrlParams } = provider;
341
- let finalAuthUrl = authorizationUrl;
342
- if (!finalAuthUrl) {
343
- if (!discoveryUrl) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
344
- const discovery = await betterFetch(discoveryUrl, {
345
- method: "GET",
346
- headers: provider.discoveryHeaders,
347
- onError(context) {
348
- c.context.logger.error(context.error.message, context.error, { discoveryUrl });
349
- }
350
- });
351
- if (discovery.data) finalAuthUrl = discovery.data.authorization_endpoint;
352
- }
353
- if (!finalAuthUrl) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
354
- const state = await generateState(c, {
355
- userId: session.user.id,
356
- email: session.user.email
357
- }, void 0);
358
- const additionalParams = typeof authorizationUrlParams === "function" ? authorizationUrlParams(c) : authorizationUrlParams;
359
- const url = await createAuthorizationURL({
360
- id: providerId,
361
- options: {
362
- clientId,
363
- clientSecret,
364
- redirectURI: redirectURI || `${c.context.baseURL}/oauth2/callback/${providerId}`
365
- },
366
- authorizationEndpoint: finalAuthUrl,
367
- state: state.state,
368
- codeVerifier: pkce ? state.codeVerifier : void 0,
369
- scopes: c.body.scopes || scopes || [],
370
- redirectURI: redirectURI || `${c.context.baseURL}/oauth2/callback/${providerId}`,
371
- prompt,
372
- accessType,
373
- additionalParams
374
- });
375
- return c.json({
376
- url: url.toString(),
377
- redirect: true
378
- });
379
- });
380
- async function getUserInfo(tokens, finalUserInfoUrl) {
381
- if (tokens.idToken) {
382
- const decoded = decodeJwt(tokens.idToken);
383
- if (decoded) {
384
- if (decoded.sub && decoded.email) return {
385
- id: decoded.sub,
386
- emailVerified: decoded.email_verified,
387
- image: decoded.picture,
388
- ...decoded
389
- };
390
- }
391
- }
392
- if (!finalUserInfoUrl) return null;
393
- const userInfo = await betterFetch(finalUserInfoUrl, {
394
- method: "GET",
395
- headers: { Authorization: `Bearer ${tokens.accessToken}` }
396
- });
397
- return {
398
- id: userInfo.data?.sub ?? "",
399
- emailVerified: userInfo.data?.email_verified ?? false,
400
- email: userInfo.data?.email,
401
- image: userInfo.data?.picture,
402
- name: userInfo.data?.name,
403
- ...userInfo.data
404
- };
405
- }
406
- //#endregion
407
- export { getUserInfo, oAuth2Callback, oAuth2LinkAccount, signInWithOAuth2 };