better-auth 1.6.12 → 1.6.13
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 -46
- package/dist/api/routes/account.d.mts +2 -23
- package/dist/api/routes/account.mjs +94 -73
- package/dist/api/routes/callback.mjs +3 -2
- package/dist/api/routes/password.mjs +1 -1
- package/dist/api/routes/session.mjs +1 -1
- package/dist/api/routes/sign-in.mjs +1 -1
- package/dist/api/routes/update-user.mjs +3 -3
- package/dist/client/fetch-plugins.mjs +2 -1
- package/dist/context/create-context.mjs +10 -14
- package/dist/db/internal-adapter.mjs +19 -20
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +3 -3
- package/dist/oauth2/link-account.d.mts +27 -1
- package/dist/oauth2/link-account.mjs +24 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +3 -3
- package/dist/plugins/anonymous/index.mjs +2 -2
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/generic-oauth/routes.mjs +3 -2
- package/dist/plugins/mcp/index.mjs +2 -1
- package/dist/plugins/oauth-proxy/index.mjs +1 -1
- package/dist/plugins/oidc-provider/index.mjs +2 -1
- package/dist/plugins/one-tap/client.mjs +9 -2
- package/dist/plugins/one-tap/index.mjs +16 -39
- package/dist/plugins/organization/routes/crud-org.d.mts +4 -4
- package/dist/plugins/organization/routes/crud-org.mjs +2 -2
- package/dist/plugins/organization/types.d.mts +3 -3
- package/dist/plugins/phone-number/routes.mjs +1 -1
- package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
- package/dist/plugins/two-factor/client.mjs +2 -1
- package/dist/test-utils/test-instance.d.mts +12 -138
- package/package.json +8 -8
package/dist/api/index.d.mts
CHANGED
|
@@ -1931,29 +1931,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
1931
1931
|
}>;
|
|
1932
1932
|
readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
|
|
1933
1933
|
method: "GET";
|
|
1934
|
-
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
|
|
1935
|
-
session: {
|
|
1936
|
-
session: Record<string, any> & {
|
|
1937
|
-
id: string;
|
|
1938
|
-
createdAt: Date;
|
|
1939
|
-
updatedAt: Date;
|
|
1940
|
-
userId: string;
|
|
1941
|
-
expiresAt: Date;
|
|
1942
|
-
token: string;
|
|
1943
|
-
ipAddress?: string | null | undefined;
|
|
1944
|
-
userAgent?: string | null | undefined;
|
|
1945
|
-
};
|
|
1946
|
-
user: Record<string, any> & {
|
|
1947
|
-
id: string;
|
|
1948
|
-
createdAt: Date;
|
|
1949
|
-
updatedAt: Date;
|
|
1950
|
-
email: string;
|
|
1951
|
-
emailVerified: boolean;
|
|
1952
|
-
name: string;
|
|
1953
|
-
image?: string | null | undefined;
|
|
1954
|
-
};
|
|
1955
|
-
};
|
|
1956
|
-
}>)[];
|
|
1957
1934
|
metadata: {
|
|
1958
1935
|
openapi: {
|
|
1959
1936
|
description: string;
|
|
@@ -2003,6 +1980,8 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
2003
1980
|
};
|
|
2004
1981
|
query: zod.ZodOptional<zod.ZodObject<{
|
|
2005
1982
|
accountId: zod.ZodOptional<zod.ZodString>;
|
|
1983
|
+
providerId: zod.ZodOptional<zod.ZodString>;
|
|
1984
|
+
userId: zod.ZodOptional<zod.ZodString>;
|
|
2006
1985
|
}, zod_v4_core0.$strip>>;
|
|
2007
1986
|
}, {
|
|
2008
1987
|
user: _better_auth_core_oauth20.OAuth2UserInfo;
|
|
@@ -3922,29 +3901,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
3922
3901
|
}>;
|
|
3923
3902
|
readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
|
|
3924
3903
|
method: "GET";
|
|
3925
|
-
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
|
|
3926
|
-
session: {
|
|
3927
|
-
session: Record<string, any> & {
|
|
3928
|
-
id: string;
|
|
3929
|
-
createdAt: Date;
|
|
3930
|
-
updatedAt: Date;
|
|
3931
|
-
userId: string;
|
|
3932
|
-
expiresAt: Date;
|
|
3933
|
-
token: string;
|
|
3934
|
-
ipAddress?: string | null | undefined;
|
|
3935
|
-
userAgent?: string | null | undefined;
|
|
3936
|
-
};
|
|
3937
|
-
user: Record<string, any> & {
|
|
3938
|
-
id: string;
|
|
3939
|
-
createdAt: Date;
|
|
3940
|
-
updatedAt: Date;
|
|
3941
|
-
email: string;
|
|
3942
|
-
emailVerified: boolean;
|
|
3943
|
-
name: string;
|
|
3944
|
-
image?: string | null | undefined;
|
|
3945
|
-
};
|
|
3946
|
-
};
|
|
3947
|
-
}>)[];
|
|
3948
3904
|
metadata: {
|
|
3949
3905
|
openapi: {
|
|
3950
3906
|
description: string;
|
|
@@ -3994,6 +3950,8 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
3994
3950
|
};
|
|
3995
3951
|
query: zod.ZodOptional<zod.ZodObject<{
|
|
3996
3952
|
accountId: zod.ZodOptional<zod.ZodString>;
|
|
3953
|
+
providerId: zod.ZodOptional<zod.ZodString>;
|
|
3954
|
+
userId: zod.ZodOptional<zod.ZodString>;
|
|
3997
3955
|
}, zod_v4_core0.$strip>>;
|
|
3998
3956
|
}, {
|
|
3999
3957
|
user: _better_auth_core_oauth20.OAuth2UserInfo;
|
|
@@ -328,29 +328,6 @@ declare const refreshToken: better_call0.StrictEndpoint<"/refresh-token", {
|
|
|
328
328
|
}>;
|
|
329
329
|
declare const accountInfo: better_call0.StrictEndpoint<"/account-info", {
|
|
330
330
|
method: "GET";
|
|
331
|
-
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
|
|
332
|
-
session: {
|
|
333
|
-
session: Record<string, any> & {
|
|
334
|
-
id: string;
|
|
335
|
-
createdAt: Date;
|
|
336
|
-
updatedAt: Date;
|
|
337
|
-
userId: string;
|
|
338
|
-
expiresAt: Date;
|
|
339
|
-
token: string;
|
|
340
|
-
ipAddress?: string | null | undefined;
|
|
341
|
-
userAgent?: string | null | undefined;
|
|
342
|
-
};
|
|
343
|
-
user: Record<string, any> & {
|
|
344
|
-
id: string;
|
|
345
|
-
createdAt: Date;
|
|
346
|
-
updatedAt: Date;
|
|
347
|
-
email: string;
|
|
348
|
-
emailVerified: boolean;
|
|
349
|
-
name: string;
|
|
350
|
-
image?: string | null | undefined;
|
|
351
|
-
};
|
|
352
|
-
};
|
|
353
|
-
}>)[];
|
|
354
331
|
metadata: {
|
|
355
332
|
openapi: {
|
|
356
333
|
description: string;
|
|
@@ -400,6 +377,8 @@ declare const accountInfo: better_call0.StrictEndpoint<"/account-info", {
|
|
|
400
377
|
};
|
|
401
378
|
query: z.ZodOptional<z.ZodObject<{
|
|
402
379
|
accountId: z.ZodOptional<z.ZodString>;
|
|
380
|
+
providerId: z.ZodOptional<z.ZodString>;
|
|
381
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
403
382
|
}, z.core.$strip>>;
|
|
404
383
|
}, {
|
|
405
384
|
user: _better_auth_core_oauth20.OAuth2UserInfo;
|
|
@@ -2,8 +2,9 @@ import { parseAccountOutput } from "../../db/schema.mjs";
|
|
|
2
2
|
import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
|
|
3
3
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
4
4
|
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
5
|
-
import { generateState } from "../../oauth2/state.mjs";
|
|
6
5
|
import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils.mjs";
|
|
6
|
+
import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
|
|
7
|
+
import { generateState } from "../../oauth2/state.mjs";
|
|
7
8
|
import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "./session.mjs";
|
|
8
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
10
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -166,14 +167,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
166
167
|
code: "LINKING_FAILED"
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
await c.context.internalAdapter.updateUser(session.user.id, {
|
|
171
|
-
name: linkingUserInfo.user?.name,
|
|
172
|
-
image: linkingUserInfo.user?.image
|
|
173
|
-
});
|
|
174
|
-
} catch (e) {
|
|
175
|
-
console.warn("Could not update user - " + e.toString());
|
|
176
|
-
}
|
|
170
|
+
await applyUpdateUserInfoOnLink(c, session.user.id, linkingUserInfo.user);
|
|
177
171
|
return c.json({
|
|
178
172
|
url: "",
|
|
179
173
|
status: true,
|
|
@@ -222,50 +216,44 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
|
|
|
222
216
|
await ctx.context.internalAdapter.deleteAccount(accountExist.id);
|
|
223
217
|
return ctx.json({ status: true });
|
|
224
218
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
description: "A Valid access token",
|
|
237
|
-
content: { "application/json": { schema: {
|
|
238
|
-
type: "object",
|
|
239
|
-
properties: {
|
|
240
|
-
tokenType: { type: "string" },
|
|
241
|
-
idToken: { type: "string" },
|
|
242
|
-
accessToken: { type: "string" },
|
|
243
|
-
accessTokenExpiresAt: {
|
|
244
|
-
type: "string",
|
|
245
|
-
format: "date-time"
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
} } }
|
|
249
|
-
},
|
|
250
|
-
400: { description: "Invalid refresh token or provider configuration" }
|
|
251
|
-
}
|
|
252
|
-
} }
|
|
253
|
-
}, async (ctx) => {
|
|
254
|
-
const { providerId, accountId, userId } = ctx.body || {};
|
|
255
|
-
const req = ctx.request;
|
|
219
|
+
/**
|
|
220
|
+
* Resolves the user id an account-token operation should act on.
|
|
221
|
+
*
|
|
222
|
+
* A caller reaching the server over HTTP (a request or session headers are
|
|
223
|
+
* present) must have a valid session, and that session's user always wins.
|
|
224
|
+
* A trusted server-side `auth.api` caller with no session may instead name a
|
|
225
|
+
* `userId` directly. Throws `UNAUTHORIZED` when an HTTP caller is
|
|
226
|
+
* unauthenticated, and `USER_ID_OR_SESSION_REQUIRED` when neither a session
|
|
227
|
+
* nor a `userId` is available.
|
|
228
|
+
*/
|
|
229
|
+
async function resolveUserId(ctx, userId) {
|
|
256
230
|
const session = await getSessionFromCtx(ctx);
|
|
257
|
-
if (
|
|
231
|
+
if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
|
|
258
232
|
const resolvedUserId = session?.user?.id || userId;
|
|
259
|
-
if (!resolvedUserId) throw
|
|
233
|
+
if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
|
|
234
|
+
message: "Either userId or session is required",
|
|
235
|
+
code: "USER_ID_OR_SESSION_REQUIRED"
|
|
236
|
+
});
|
|
237
|
+
return resolvedUserId;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Fetches a currently-valid access token for a user's provider account,
|
|
241
|
+
* refreshing and persisting it when it is within five seconds of expiry.
|
|
242
|
+
* Shared by the `/get-access-token` endpoint and `/account-info` so both
|
|
243
|
+
* resolve and refresh tokens through one path.
|
|
244
|
+
*/
|
|
245
|
+
async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId, account: resolvedAccount }) {
|
|
260
246
|
const provider = await getAwaitableValue(ctx.context.socialProviders, { value: providerId });
|
|
261
247
|
if (!provider) throw APIError.from("BAD_REQUEST", {
|
|
262
248
|
message: `Provider ${providerId} is not supported.`,
|
|
263
249
|
code: "PROVIDER_NOT_SUPPORTED"
|
|
264
250
|
});
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
251
|
+
let account = resolvedAccount;
|
|
252
|
+
if (!account) {
|
|
253
|
+
const accountData = await getAccountCookie(ctx);
|
|
254
|
+
if (accountData && accountData.userId === resolvedUserId && providerId === accountData.providerId && (!accountId || accountData.accountId === accountId)) account = accountData;
|
|
255
|
+
else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
256
|
+
}
|
|
269
257
|
if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
270
258
|
try {
|
|
271
259
|
let newTokens = null;
|
|
@@ -297,19 +285,55 @@ const getAccessToken = createAuthEndpoint("/get-access-token", {
|
|
|
297
285
|
return account.accessTokenExpiresAt;
|
|
298
286
|
}
|
|
299
287
|
})();
|
|
300
|
-
|
|
288
|
+
return {
|
|
301
289
|
accessToken: newTokens?.accessToken ?? await decryptOAuthToken(account.accessToken ?? "", ctx.context),
|
|
302
290
|
accessTokenExpiresAt,
|
|
303
291
|
scopes: account.scope?.split(",") ?? [],
|
|
304
292
|
idToken: newTokens?.idToken ?? account.idToken ?? void 0
|
|
305
293
|
};
|
|
306
|
-
return ctx.json(tokens);
|
|
307
294
|
} catch (_error) {
|
|
308
295
|
throw APIError.from("BAD_REQUEST", {
|
|
309
296
|
message: "Failed to get a valid access token",
|
|
310
297
|
code: "FAILED_TO_GET_ACCESS_TOKEN"
|
|
311
298
|
});
|
|
312
299
|
}
|
|
300
|
+
}
|
|
301
|
+
const getAccessToken = createAuthEndpoint("/get-access-token", {
|
|
302
|
+
method: "POST",
|
|
303
|
+
body: z.object({
|
|
304
|
+
providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }),
|
|
305
|
+
accountId: z.string().meta({ description: "The account ID associated with the refresh token" }).optional(),
|
|
306
|
+
userId: z.string().meta({ description: "The user ID associated with the account" }).optional()
|
|
307
|
+
}),
|
|
308
|
+
metadata: { openapi: {
|
|
309
|
+
description: "Get a valid access token, doing a refresh if needed",
|
|
310
|
+
responses: {
|
|
311
|
+
200: {
|
|
312
|
+
description: "A Valid access token",
|
|
313
|
+
content: { "application/json": { schema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
tokenType: { type: "string" },
|
|
317
|
+
idToken: { type: "string" },
|
|
318
|
+
accessToken: { type: "string" },
|
|
319
|
+
accessTokenExpiresAt: {
|
|
320
|
+
type: "string",
|
|
321
|
+
format: "date-time"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} } }
|
|
325
|
+
},
|
|
326
|
+
400: { description: "Invalid refresh token or provider configuration" }
|
|
327
|
+
}
|
|
328
|
+
} }
|
|
329
|
+
}, async (ctx) => {
|
|
330
|
+
const { providerId, accountId, userId } = ctx.body || {};
|
|
331
|
+
const tokens = await getValidAccessToken(ctx, {
|
|
332
|
+
resolvedUserId: await resolveUserId(ctx, userId),
|
|
333
|
+
providerId,
|
|
334
|
+
accountId
|
|
335
|
+
});
|
|
336
|
+
return ctx.json(tokens);
|
|
313
337
|
});
|
|
314
338
|
const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
315
339
|
method: "POST",
|
|
@@ -346,14 +370,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
346
370
|
} }
|
|
347
371
|
}, async (ctx) => {
|
|
348
372
|
const { providerId, accountId, userId } = ctx.body;
|
|
349
|
-
const
|
|
350
|
-
const session = await getSessionFromCtx(ctx);
|
|
351
|
-
if (req && !session) throw ctx.error("UNAUTHORIZED");
|
|
352
|
-
const resolvedUserId = session?.user?.id || userId;
|
|
353
|
-
if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
|
|
354
|
-
message: `Either userId or session is required`,
|
|
355
|
-
code: "USER_ID_OR_SESSION_REQUIRED"
|
|
356
|
-
});
|
|
373
|
+
const resolvedUserId = await resolveUserId(ctx, userId);
|
|
357
374
|
const provider = await getAwaitableValue(ctx.context.socialProviders, { value: providerId });
|
|
358
375
|
if (!provider) throw APIError.from("BAD_REQUEST", {
|
|
359
376
|
message: `Provider ${providerId} is not supported.`,
|
|
@@ -418,10 +435,13 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
418
435
|
});
|
|
419
436
|
}
|
|
420
437
|
});
|
|
421
|
-
const accountInfoQuerySchema = z.optional(z.object({
|
|
438
|
+
const accountInfoQuerySchema = z.optional(z.object({
|
|
439
|
+
accountId: z.string().meta({ description: "The provider given account id for which to get the account info" }).optional(),
|
|
440
|
+
providerId: z.string().meta({ description: "The provider ID to disambiguate provider-issued account IDs" }).optional(),
|
|
441
|
+
userId: z.string().meta({ description: "The user ID associated with the account" }).optional()
|
|
442
|
+
}));
|
|
422
443
|
const accountInfo = createAuthEndpoint("/account-info", {
|
|
423
444
|
method: "GET",
|
|
424
|
-
use: [sessionMiddleware],
|
|
425
445
|
metadata: { openapi: {
|
|
426
446
|
description: "Get the account info provided by the provider",
|
|
427
447
|
responses: { "200": {
|
|
@@ -453,7 +473,8 @@ const accountInfo = createAuthEndpoint("/account-info", {
|
|
|
453
473
|
} },
|
|
454
474
|
query: accountInfoQuerySchema
|
|
455
475
|
}, async (ctx) => {
|
|
456
|
-
const providedAccountId = ctx.query
|
|
476
|
+
const { accountId: providedAccountId, providerId: providedProviderId, userId } = ctx.query || {};
|
|
477
|
+
const resolvedUserId = await resolveUserId(ctx, userId);
|
|
457
478
|
let account = void 0;
|
|
458
479
|
if (!providedAccountId) {
|
|
459
480
|
if (ctx.context.options.account?.storeAccountCookie) {
|
|
@@ -461,24 +482,24 @@ const accountInfo = createAuthEndpoint("/account-info", {
|
|
|
461
482
|
if (accountData) account = accountData;
|
|
462
483
|
}
|
|
463
484
|
} else {
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
485
|
+
const matchingAccounts = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).filter((acc) => acc.accountId === providedAccountId && (!providedProviderId || acc.providerId === providedProviderId));
|
|
486
|
+
if (matchingAccounts.length > 1) throw APIError.from("BAD_REQUEST", {
|
|
487
|
+
message: "Multiple accounts share this account ID. Pass a providerId to disambiguate.",
|
|
488
|
+
code: "AMBIGUOUS_ACCOUNT"
|
|
489
|
+
});
|
|
490
|
+
account = matchingAccounts[0];
|
|
466
491
|
}
|
|
467
|
-
if (!account || account.userId !==
|
|
492
|
+
if (!account || account.userId !== resolvedUserId) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
468
493
|
const provider = await getAwaitableValue(ctx.context.socialProviders, { value: account.providerId });
|
|
469
|
-
if (!provider) throw APIError.from("
|
|
470
|
-
message:
|
|
494
|
+
if (!provider) throw APIError.from("BAD_REQUEST", {
|
|
495
|
+
message: "Account is not associated with a configured social provider.",
|
|
471
496
|
code: "PROVIDER_NOT_CONFIGURED"
|
|
472
497
|
});
|
|
473
|
-
const tokens = await
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
providerId: account.providerId
|
|
479
|
-
},
|
|
480
|
-
returnHeaders: false,
|
|
481
|
-
returnStatus: false
|
|
498
|
+
const tokens = await getValidAccessToken(ctx, {
|
|
499
|
+
resolvedUserId,
|
|
500
|
+
providerId: account.providerId,
|
|
501
|
+
accountId: account.accountId,
|
|
502
|
+
account
|
|
482
503
|
});
|
|
483
504
|
if (!tokens.accessToken) throw APIError.from("BAD_REQUEST", {
|
|
484
505
|
message: "Access token not found",
|
|
@@ -2,9 +2,9 @@ import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
3
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
4
4
|
import { missingEmailLogMessage, redirectOnError } from "../../oauth2/errors.mjs";
|
|
5
|
-
import { parseState } from "../../oauth2/state.mjs";
|
|
6
5
|
import { setTokenUtil } from "../../oauth2/utils.mjs";
|
|
7
|
-
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
6
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
7
|
+
import { parseState } from "../../oauth2/state.mjs";
|
|
8
8
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
9
9
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
10
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -117,6 +117,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
117
117
|
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
118
118
|
scope: tokens.scopes?.join(",")
|
|
119
119
|
})) redirectOnError(c, resolvedErrorURL, "unable_to_link_account");
|
|
120
|
+
await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
|
|
120
121
|
let toRedirectTo;
|
|
121
122
|
try {
|
|
122
123
|
toRedirectTo = callbackURL.toString();
|
|
@@ -161,7 +161,7 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
161
161
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
162
162
|
if (user) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
163
163
|
}
|
|
164
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.
|
|
164
|
+
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
165
165
|
return ctx.json({ status: true });
|
|
166
166
|
});
|
|
167
167
|
const verifyPassword = createAuthEndpoint("/verify-password", {
|
|
@@ -445,7 +445,7 @@ const revokeSessions = createAuthEndpoint("/revoke-sessions", {
|
|
|
445
445
|
} }
|
|
446
446
|
}, async (ctx) => {
|
|
447
447
|
try {
|
|
448
|
-
await ctx.context.internalAdapter.
|
|
448
|
+
await ctx.context.internalAdapter.deleteUserSessions(ctx.context.session.user.id);
|
|
449
449
|
} catch (error) {
|
|
450
450
|
ctx.context.logger.error(error && typeof error === "object" && "name" in error ? error.name : "", error);
|
|
451
451
|
throw APIError.from("INTERNAL_SERVER_ERROR", {
|
|
@@ -3,8 +3,8 @@ import { parseUserOutput } from "../../db/schema.mjs";
|
|
|
3
3
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
4
4
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
5
5
|
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
6
|
-
import { generateState } from "../../oauth2/state.mjs";
|
|
7
6
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
7
|
+
import { generateState } from "../../oauth2/state.mjs";
|
|
8
8
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
9
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
10
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -168,7 +168,7 @@ const changePassword = createAuthEndpoint("/change-password", {
|
|
|
168
168
|
await ctx.context.internalAdapter.updateAccount(account.id, { password: passwordHash });
|
|
169
169
|
let token = null;
|
|
170
170
|
if (revokeOtherSessions) {
|
|
171
|
-
await ctx.context.internalAdapter.
|
|
171
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
172
172
|
const newSession = await ctx.context.internalAdapter.createSession(session.user.id);
|
|
173
173
|
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
174
174
|
await setSessionCookie(ctx, {
|
|
@@ -309,7 +309,7 @@ const deleteUser = createAuthEndpoint("/delete-user", {
|
|
|
309
309
|
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
|
310
310
|
if (beforeDelete) await beforeDelete(session.user, ctx.request);
|
|
311
311
|
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
312
|
-
await ctx.context.internalAdapter.
|
|
312
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
313
313
|
deleteSessionCookie(ctx);
|
|
314
314
|
const afterDelete = ctx.context.options.user.deleteUser?.afterDelete;
|
|
315
315
|
if (afterDelete) await afterDelete(session.user, ctx.request);
|
|
@@ -362,7 +362,7 @@ const deleteUserCallback = createAuthEndpoint("/delete-user/callback", {
|
|
|
362
362
|
const beforeDelete = ctx.context.options.user.deleteUser?.beforeDelete;
|
|
363
363
|
if (beforeDelete) await beforeDelete(session.user, ctx.request);
|
|
364
364
|
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
365
|
-
await ctx.context.internalAdapter.
|
|
365
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
366
366
|
await ctx.context.internalAdapter.deleteAccounts(session.user.id);
|
|
367
367
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(`delete-account-${ctx.query.token}`);
|
|
368
368
|
deleteSessionCookie(ctx);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
|
|
1
2
|
//#region src/client/fetch-plugins.ts
|
|
2
3
|
const redirectPlugin = {
|
|
3
4
|
id: "redirect",
|
|
4
5
|
name: "Redirect",
|
|
5
6
|
hooks: { onSuccess(context) {
|
|
6
|
-
if (context.data?.url && context.data?.redirect) {
|
|
7
|
+
if (context.data?.url && context.data?.redirect && isSafeUrlScheme(context.data.url)) {
|
|
7
8
|
if (typeof window !== "undefined" && window.location) {
|
|
8
9
|
if (window.location) try {
|
|
9
10
|
window.location.href = context.data.url;
|
|
@@ -42,18 +42,14 @@ function validateSecret(secret, logger) {
|
|
|
42
42
|
if (estimateEntropy(secret) < 120) logger.warn("[better-auth] Warning: your BETTER_AUTH_SECRET appears low-entropy. Use a randomly generated secret for production.");
|
|
43
43
|
}
|
|
44
44
|
async function createAuthContext(adapter, options, getDatabaseType) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
storeStateStrategy: "cookie",
|
|
54
|
-
storeAccountCookie: true
|
|
55
|
-
}
|
|
56
|
-
});
|
|
45
|
+
const isStateful = !!options.database || !!options.secondaryStorage;
|
|
46
|
+
if (!isStateful) options = defu$1(options, { session: { cookieCache: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
strategy: "jwe",
|
|
49
|
+
refreshCache: true,
|
|
50
|
+
maxAge: options.session?.expiresIn || 3600 * 24 * 7
|
|
51
|
+
} } });
|
|
52
|
+
if (!options.database) options = defu$1(options, { account: { storeAccountCookie: true } });
|
|
57
53
|
const plugins = options.plugins || [];
|
|
58
54
|
const internalPlugins = getInternalPlugins(options);
|
|
59
55
|
const logger = createLogger(options.logger);
|
|
@@ -130,7 +126,7 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
130
126
|
socialProviders: providers,
|
|
131
127
|
options,
|
|
132
128
|
oauthConfig: {
|
|
133
|
-
storeStateStrategy: options.account?.storeStateStrategy || (
|
|
129
|
+
storeStateStrategy: options.account?.storeStateStrategy || (isStateful ? "database" : "cookie"),
|
|
134
130
|
skipStateCookieCheck: !!options.account?.skipStateCookieCheck
|
|
135
131
|
},
|
|
136
132
|
tables,
|
|
@@ -146,7 +142,7 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
146
142
|
cookieRefreshCache: (() => {
|
|
147
143
|
const refreshCache = options.session?.cookieCache?.refreshCache;
|
|
148
144
|
const maxAge = options.session?.cookieCache?.maxAge || 300;
|
|
149
|
-
if (
|
|
145
|
+
if (isStateful && refreshCache) {
|
|
150
146
|
logger.warn("[better-auth] `session.cookieCache.refreshCache` is enabled while `database` or `secondaryStorage` is configured. `refreshCache` is meant for stateless (DB-less) setups. Disabling `refreshCache` — remove it from your config to silence this warning.");
|
|
151
147
|
return false;
|
|
152
148
|
}
|
|
@@ -388,21 +388,29 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
388
388
|
value: id
|
|
389
389
|
}], "account", void 0);
|
|
390
390
|
},
|
|
391
|
-
|
|
391
|
+
deleteUserSessions: async (userId) => {
|
|
392
392
|
if (secondaryStorage) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
await secondaryStorage.delete(`active-sessions-${userIdOrSessionTokens}`);
|
|
399
|
-
} else for (const sessionToken of userIdOrSessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken);
|
|
393
|
+
const activeSession = await secondaryStorage.get(`active-sessions-${userId}`);
|
|
394
|
+
const sessions = activeSession ? safeJSONParse(activeSession) : [];
|
|
395
|
+
if (!sessions) return;
|
|
396
|
+
for (const session of sessions) await secondaryStorage.delete(session.token);
|
|
397
|
+
await secondaryStorage.delete(`active-sessions-${userId}`);
|
|
400
398
|
if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return;
|
|
401
399
|
}
|
|
402
400
|
await deleteManyWithHooks([{
|
|
403
|
-
field:
|
|
404
|
-
value:
|
|
405
|
-
|
|
401
|
+
field: "userId",
|
|
402
|
+
value: userId
|
|
403
|
+
}], "session", void 0);
|
|
404
|
+
},
|
|
405
|
+
deleteSessions: async (sessionTokens) => {
|
|
406
|
+
if (secondaryStorage) {
|
|
407
|
+
for (const sessionToken of sessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken);
|
|
408
|
+
if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return;
|
|
409
|
+
}
|
|
410
|
+
await deleteManyWithHooks([{
|
|
411
|
+
field: "token",
|
|
412
|
+
value: sessionTokens,
|
|
413
|
+
operator: "in"
|
|
406
414
|
}], "session", void 0);
|
|
407
415
|
},
|
|
408
416
|
findOAuthUser: async (email, accountId, providerId) => {
|
|
@@ -532,15 +540,6 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
532
540
|
}]
|
|
533
541
|
});
|
|
534
542
|
},
|
|
535
|
-
findAccount: async (accountId) => {
|
|
536
|
-
return await (await getCurrentAdapter(adapter)).findOne({
|
|
537
|
-
model: "account",
|
|
538
|
-
where: [{
|
|
539
|
-
field: "accountId",
|
|
540
|
-
value: accountId
|
|
541
|
-
}]
|
|
542
|
-
});
|
|
543
|
-
},
|
|
544
543
|
findAccountByProviderId: async (accountId, providerId) => {
|
|
545
544
|
return await (await getCurrentAdapter(adapter)).findOne({
|
|
546
545
|
model: "account",
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateState, parseState } from "./state.mjs";
|
|
2
|
-
import { handleOAuthUserInfo } from "./link-account.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
3
3
|
import { decryptOAuthToken, setTokenUtil } from "./utils.mjs";
|
|
4
4
|
export * from "@better-auth/core/oauth2";
|
|
5
|
-
export { decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
5
|
+
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { generateState, parseState } from "./state.mjs";
|
|
2
1
|
import { decryptOAuthToken, setTokenUtil } from "./utils.mjs";
|
|
3
|
-
import { handleOAuthUserInfo } from "./link-account.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
3
|
+
import { generateState, parseState } from "./state.mjs";
|
|
4
4
|
export * from "@better-auth/core/oauth2";
|
|
5
|
-
export { decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
5
|
+
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateState, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
@@ -42,5 +42,31 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
|
42
42
|
error: null;
|
|
43
43
|
isRegister: boolean;
|
|
44
44
|
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Provider profile a freshly linked account may copy onto the local user.
|
|
47
|
+
* `id` is the provider's account id (never the local user id), and `email`/
|
|
48
|
+
* `emailVerified` are identity anchors; all three are stripped before the
|
|
49
|
+
* remaining fields are written.
|
|
50
|
+
*/
|
|
51
|
+
type LinkedProviderProfile = {
|
|
52
|
+
id: string | number;
|
|
53
|
+
name?: string | undefined;
|
|
54
|
+
email?: string | null | undefined;
|
|
55
|
+
emailVerified?: boolean | undefined;
|
|
56
|
+
image?: string | null | undefined;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
|
60
|
+
* copy the freshly linked provider's profile onto the local user, matching the
|
|
61
|
+
* field set persisted on sign-up. The local `email` and `emailVerified` are
|
|
62
|
+
* never changed, so a link can't rebind the account's identity, and
|
|
63
|
+
* `updateUser` drops `undefined` fields, so a provider that omits one leaves
|
|
64
|
+
* the existing column intact.
|
|
65
|
+
*
|
|
66
|
+
* Returns the updated user so a caller that issues a session can seed the
|
|
67
|
+
* cookie cache with the fresh row. Returns `undefined` when the policy is
|
|
68
|
+
* disabled or the update fails: a failed profile sync must not abort the link.
|
|
69
|
+
*/
|
|
70
|
+
declare function applyUpdateUserInfoOnLink(c: GenericEndpointContext, userId: string, userInfo: LinkedProviderProfile): Promise<User | undefined>;
|
|
45
71
|
//#endregion
|
|
46
|
-
export { handleOAuthUserInfo };
|
|
72
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
|
@@ -46,6 +46,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
|
|
49
|
+
user = await applyUpdateUserInfoOnLink(c, dbUser.user.id, userInfo) ?? user;
|
|
49
50
|
} else {
|
|
50
51
|
const freshTokens = c.context.options.account?.updateAccountOnSignIn !== false ? Object.fromEntries(Object.entries({
|
|
51
52
|
idToken: account.idToken,
|
|
@@ -137,5 +138,27 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
137
138
|
isRegister
|
|
138
139
|
};
|
|
139
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
|
143
|
+
* copy the freshly linked provider's profile onto the local user, matching the
|
|
144
|
+
* field set persisted on sign-up. The local `email` and `emailVerified` are
|
|
145
|
+
* never changed, so a link can't rebind the account's identity, and
|
|
146
|
+
* `updateUser` drops `undefined` fields, so a provider that omits one leaves
|
|
147
|
+
* the existing column intact.
|
|
148
|
+
*
|
|
149
|
+
* Returns the updated user so a caller that issues a session can seed the
|
|
150
|
+
* cookie cache with the fresh row. Returns `undefined` when the policy is
|
|
151
|
+
* disabled or the update fails: a failed profile sync must not abort the link.
|
|
152
|
+
*/
|
|
153
|
+
async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
|
|
154
|
+
if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return;
|
|
155
|
+
const { id: _id, email: _email, emailVerified: _emailVerified, ...profile } = userInfo;
|
|
156
|
+
try {
|
|
157
|
+
return await c.context.internalAdapter.updateUser(userId, profile);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
c.context.logger.warn("Could not update user info on account link", e);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
140
163
|
//#endregion
|
|
141
|
-
export { handleOAuthUserInfo };
|
|
164
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|