better-auth 1.6.11 → 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 +12 -48
- package/dist/api/routes/account.d.mts +2 -23
- package/dist/api/routes/account.mjs +94 -73
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +39 -42
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/password.mjs +1 -1
- package/dist/api/routes/session.mjs +15 -10
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +3 -2
- package/dist/api/routes/sign-up.d.mts +1 -0
- package/dist/api/routes/sign-up.mjs +9 -7
- package/dist/api/routes/update-user.mjs +7 -7
- package/dist/client/fetch-plugins.mjs +2 -1
- package/dist/client/parser.mjs +0 -1
- package/dist/client/plugins/index.d.mts +3 -3
- package/dist/client/proxy.mjs +2 -1
- package/dist/context/create-context.mjs +10 -14
- package/dist/context/helpers.mjs +3 -2
- package/dist/cookies/cookie-utils.d.mts +24 -1
- package/dist/cookies/cookie-utils.mjs +85 -22
- package/dist/cookies/index.d.mts +2 -3
- package/dist/cookies/index.mjs +39 -11
- package/dist/cookies/session-store.mjs +4 -23
- package/dist/db/get-migration.mjs +4 -4
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +3 -2
- package/dist/db/internal-adapter.mjs +56 -50
- package/dist/db/schema.d.mts +15 -2
- package/dist/db/schema.mjs +26 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +16 -1
- 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 +27 -4
- package/dist/oauth2/state.mjs +8 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +11 -6
- package/dist/plugins/admin/admin.mjs +0 -4
- package/dist/plugins/admin/client.d.mts +1 -1
- package/dist/plugins/admin/routes.mjs +3 -3
- package/dist/plugins/anonymous/index.mjs +2 -2
- package/dist/plugins/bearer/index.mjs +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +6 -6
- package/dist/plugins/generic-oauth/routes.mjs +37 -34
- package/dist/plugins/generic-oauth/types.d.mts +7 -0
- package/dist/plugins/last-login-method/client.mjs +2 -2
- package/dist/plugins/magic-link/index.mjs +0 -1
- package/dist/plugins/mcp/index.mjs +2 -5
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +45 -32
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/index.mjs +2 -5
- package/dist/plugins/one-tap/client.mjs +9 -2
- package/dist/plugins/one-tap/index.mjs +16 -39
- package/dist/plugins/open-api/generator.mjs +16 -5
- package/dist/plugins/organization/adapter.mjs +61 -56
- package/dist/plugins/organization/client.d.mts +2 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
- 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/plugins/two-factor/index.mjs +3 -2
- package/dist/plugins/username/index.d.mts +24 -2
- package/dist/plugins/username/index.mjs +49 -3
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +18 -4
- package/dist/test-utils/headers.mjs +2 -7
- package/dist/test-utils/test-instance.d.mts +36 -144
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +9 -3
- package/package.json +15 -14
package/dist/api/index.d.mts
CHANGED
|
@@ -228,7 +228,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
228
228
|
allowedMediaTypes: string[];
|
|
229
229
|
scope: "server";
|
|
230
230
|
};
|
|
231
|
-
},
|
|
231
|
+
}, never>;
|
|
232
232
|
readonly getSession: better_call0.StrictEndpoint<"/get-session", {
|
|
233
233
|
method: ("GET" | "POST")[];
|
|
234
234
|
operationId: string;
|
|
@@ -327,6 +327,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
327
327
|
callbackURL: zod.ZodOptional<zod.ZodString>;
|
|
328
328
|
rememberMe: zod.ZodOptional<zod.ZodBoolean>;
|
|
329
329
|
}, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
|
|
330
|
+
cloneRequest: true;
|
|
330
331
|
metadata: {
|
|
331
332
|
allowedMediaTypes: string[];
|
|
332
333
|
$Infer: {
|
|
@@ -493,6 +494,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
493
494
|
method: "POST";
|
|
494
495
|
operationId: string;
|
|
495
496
|
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
|
|
497
|
+
cloneRequest: true;
|
|
496
498
|
body: zod.ZodObject<{
|
|
497
499
|
email: zod.ZodString;
|
|
498
500
|
password: zod.ZodString;
|
|
@@ -724,6 +726,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
724
726
|
readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
|
|
725
727
|
method: "POST";
|
|
726
728
|
operationId: string;
|
|
729
|
+
cloneRequest: true;
|
|
727
730
|
body: zod.ZodObject<{
|
|
728
731
|
email: zod.ZodEmail;
|
|
729
732
|
callbackURL: zod.ZodOptional<zod.ZodString>;
|
|
@@ -1928,29 +1931,6 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
1928
1931
|
}>;
|
|
1929
1932
|
readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
|
|
1930
1933
|
method: "GET";
|
|
1931
|
-
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
|
|
1932
|
-
session: {
|
|
1933
|
-
session: Record<string, any> & {
|
|
1934
|
-
id: string;
|
|
1935
|
-
createdAt: Date;
|
|
1936
|
-
updatedAt: Date;
|
|
1937
|
-
userId: string;
|
|
1938
|
-
expiresAt: Date;
|
|
1939
|
-
token: string;
|
|
1940
|
-
ipAddress?: string | null | undefined;
|
|
1941
|
-
userAgent?: string | null | undefined;
|
|
1942
|
-
};
|
|
1943
|
-
user: Record<string, any> & {
|
|
1944
|
-
id: string;
|
|
1945
|
-
createdAt: Date;
|
|
1946
|
-
updatedAt: Date;
|
|
1947
|
-
email: string;
|
|
1948
|
-
emailVerified: boolean;
|
|
1949
|
-
name: string;
|
|
1950
|
-
image?: string | null | undefined;
|
|
1951
|
-
};
|
|
1952
|
-
};
|
|
1953
|
-
}>)[];
|
|
1954
1934
|
metadata: {
|
|
1955
1935
|
openapi: {
|
|
1956
1936
|
description: string;
|
|
@@ -2000,6 +1980,8 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
2000
1980
|
};
|
|
2001
1981
|
query: zod.ZodOptional<zod.ZodObject<{
|
|
2002
1982
|
accountId: zod.ZodOptional<zod.ZodString>;
|
|
1983
|
+
providerId: zod.ZodOptional<zod.ZodString>;
|
|
1984
|
+
userId: zod.ZodOptional<zod.ZodString>;
|
|
2003
1985
|
}, zod_v4_core0.$strip>>;
|
|
2004
1986
|
}, {
|
|
2005
1987
|
user: _better_auth_core_oauth20.OAuth2UserInfo;
|
|
@@ -2216,7 +2198,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2216
2198
|
allowedMediaTypes: string[];
|
|
2217
2199
|
scope: "server";
|
|
2218
2200
|
};
|
|
2219
|
-
},
|
|
2201
|
+
}, never>;
|
|
2220
2202
|
readonly getSession: better_call0.StrictEndpoint<"/get-session", {
|
|
2221
2203
|
method: ("GET" | "POST")[];
|
|
2222
2204
|
operationId: string;
|
|
@@ -2315,6 +2297,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2315
2297
|
callbackURL: zod.ZodOptional<zod.ZodString>;
|
|
2316
2298
|
rememberMe: zod.ZodOptional<zod.ZodBoolean>;
|
|
2317
2299
|
}, zod_v4_core0.$strip>, zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
|
|
2300
|
+
cloneRequest: true;
|
|
2318
2301
|
metadata: {
|
|
2319
2302
|
allowedMediaTypes: string[];
|
|
2320
2303
|
$Infer: {
|
|
@@ -2481,6 +2464,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2481
2464
|
method: "POST";
|
|
2482
2465
|
operationId: string;
|
|
2483
2466
|
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
|
|
2467
|
+
cloneRequest: true;
|
|
2484
2468
|
body: zod.ZodObject<{
|
|
2485
2469
|
email: zod.ZodString;
|
|
2486
2470
|
password: zod.ZodString;
|
|
@@ -2712,6 +2696,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2712
2696
|
readonly sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
|
|
2713
2697
|
method: "POST";
|
|
2714
2698
|
operationId: string;
|
|
2699
|
+
cloneRequest: true;
|
|
2715
2700
|
body: zod.ZodObject<{
|
|
2716
2701
|
email: zod.ZodEmail;
|
|
2717
2702
|
callbackURL: zod.ZodOptional<zod.ZodString>;
|
|
@@ -3916,29 +3901,6 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
3916
3901
|
}>;
|
|
3917
3902
|
readonly accountInfo: better_call0.StrictEndpoint<"/account-info", {
|
|
3918
3903
|
method: "GET";
|
|
3919
|
-
use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
|
|
3920
|
-
session: {
|
|
3921
|
-
session: Record<string, any> & {
|
|
3922
|
-
id: string;
|
|
3923
|
-
createdAt: Date;
|
|
3924
|
-
updatedAt: Date;
|
|
3925
|
-
userId: string;
|
|
3926
|
-
expiresAt: Date;
|
|
3927
|
-
token: string;
|
|
3928
|
-
ipAddress?: string | null | undefined;
|
|
3929
|
-
userAgent?: string | null | undefined;
|
|
3930
|
-
};
|
|
3931
|
-
user: Record<string, any> & {
|
|
3932
|
-
id: string;
|
|
3933
|
-
createdAt: Date;
|
|
3934
|
-
updatedAt: Date;
|
|
3935
|
-
email: string;
|
|
3936
|
-
emailVerified: boolean;
|
|
3937
|
-
name: string;
|
|
3938
|
-
image?: string | null | undefined;
|
|
3939
|
-
};
|
|
3940
|
-
};
|
|
3941
|
-
}>)[];
|
|
3942
3904
|
metadata: {
|
|
3943
3905
|
openapi: {
|
|
3944
3906
|
description: string;
|
|
@@ -3988,6 +3950,8 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
3988
3950
|
};
|
|
3989
3951
|
query: zod.ZodOptional<zod.ZodObject<{
|
|
3990
3952
|
accountId: zod.ZodOptional<zod.ZodString>;
|
|
3953
|
+
providerId: zod.ZodOptional<zod.ZodString>;
|
|
3954
|
+
userId: zod.ZodOptional<zod.ZodString>;
|
|
3991
3955
|
}, zod_v4_core0.$strip>>;
|
|
3992
3956
|
}, {
|
|
3993
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",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
1
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
2
3
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
3
|
-
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
4
|
-
import { parseState } from "../../oauth2/state.mjs";
|
|
4
|
+
import { missingEmailLogMessage, redirectOnError } from "../../oauth2/errors.mjs";
|
|
5
5
|
import { setTokenUtil } from "../../oauth2/utils.mjs";
|
|
6
|
-
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
6
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
7
|
+
import { parseState } from "../../oauth2/state.mjs";
|
|
7
8
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
8
9
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
9
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -47,31 +48,20 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
47
48
|
else throw new Error("Unsupported method");
|
|
48
49
|
} catch (e) {
|
|
49
50
|
c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
const { code, error, state, error_description, device_id, user: userData } = queryOrBody;
|
|
53
|
-
if (!state) {
|
|
54
|
-
c.context.logger.error("State not found", error);
|
|
55
|
-
const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}state=state_not_found`;
|
|
56
|
-
throw c.redirect(url);
|
|
51
|
+
redirectOnError(c, defaultErrorURL, "invalid_callback_request");
|
|
57
52
|
}
|
|
53
|
+
const { code, error, error_description, device_id, user: userData } = queryOrBody;
|
|
58
54
|
const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp } = await parseState(c);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const params = new URLSearchParams({ error });
|
|
62
|
-
if (description) params.set("error_description", description);
|
|
63
|
-
const url = `${baseURL}${baseURL.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
64
|
-
throw c.redirect(url);
|
|
65
|
-
}
|
|
66
|
-
if (error) redirectOnError(error, error_description);
|
|
55
|
+
const resolvedErrorURL = errorURL ?? defaultErrorURL;
|
|
56
|
+
if (error) redirectOnError(c, resolvedErrorURL, error, error_description);
|
|
67
57
|
if (!code) {
|
|
68
58
|
c.context.logger.error("Code not found");
|
|
69
|
-
|
|
59
|
+
redirectOnError(c, resolvedErrorURL, "no_code");
|
|
70
60
|
}
|
|
71
61
|
const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
|
|
72
62
|
if (!provider) {
|
|
73
63
|
c.context.logger.error("Oauth provider with id", c.params.id, "not found");
|
|
74
|
-
|
|
64
|
+
redirectOnError(c, resolvedErrorURL, "oauth_provider_not_found");
|
|
75
65
|
}
|
|
76
66
|
let tokens;
|
|
77
67
|
try {
|
|
@@ -83,9 +73,9 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
83
73
|
});
|
|
84
74
|
} catch (e) {
|
|
85
75
|
c.context.logger.error("", e);
|
|
86
|
-
|
|
76
|
+
redirectOnError(c, resolvedErrorURL, "invalid_code");
|
|
87
77
|
}
|
|
88
|
-
if (!tokens)
|
|
78
|
+
if (!tokens) redirectOnError(c, resolvedErrorURL, "invalid_code");
|
|
89
79
|
const parsedUserData = userData ? safeJSONParse(userData) : null;
|
|
90
80
|
const userInfo = await provider.getUserInfo({
|
|
91
81
|
...tokens,
|
|
@@ -93,22 +83,22 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
93
83
|
}).then((res) => res?.user);
|
|
94
84
|
if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
|
|
95
85
|
c.context.logger.error("Unable to get user info");
|
|
96
|
-
|
|
86
|
+
redirectOnError(c, resolvedErrorURL, "unable_to_get_user_info");
|
|
97
87
|
}
|
|
98
88
|
const providerAccountId = String(userInfo.id);
|
|
99
89
|
if (!callbackURL) {
|
|
100
90
|
c.context.logger.error("No callback URL found");
|
|
101
|
-
|
|
91
|
+
redirectOnError(c, resolvedErrorURL, "no_callback_url");
|
|
102
92
|
}
|
|
103
93
|
if (link) {
|
|
104
94
|
if (!c.context.trustedProviders.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
105
95
|
c.context.logger.error("Unable to link account - untrusted provider");
|
|
106
|
-
|
|
96
|
+
redirectOnError(c, resolvedErrorURL, "unable_to_link_account");
|
|
107
97
|
}
|
|
108
|
-
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true)
|
|
98
|
+
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) redirectOnError(c, resolvedErrorURL, "email_doesn't_match");
|
|
109
99
|
const existingAccount = await c.context.internalAdapter.findAccountByProviderId(providerAccountId, provider.id);
|
|
110
100
|
if (existingAccount) {
|
|
111
|
-
if (existingAccount.userId.toString() !== link.userId.toString())
|
|
101
|
+
if (existingAccount.userId.toString() !== link.userId.toString()) redirectOnError(c, resolvedErrorURL, "account_already_linked_to_different_user");
|
|
112
102
|
const updateData = Object.fromEntries(Object.entries({
|
|
113
103
|
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
114
104
|
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
@@ -126,7 +116,8 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
126
116
|
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
127
117
|
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
128
118
|
scope: tokens.scopes?.join(",")
|
|
129
|
-
}))
|
|
119
|
+
})) redirectOnError(c, resolvedErrorURL, "unable_to_link_account");
|
|
120
|
+
await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
|
|
130
121
|
let toRedirectTo;
|
|
131
122
|
try {
|
|
132
123
|
toRedirectTo = callbackURL.toString();
|
|
@@ -137,7 +128,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
137
128
|
}
|
|
138
129
|
if (!userInfo.email) {
|
|
139
130
|
c.context.logger.error(missingEmailLogMessage(provider.id));
|
|
140
|
-
|
|
131
|
+
redirectOnError(c, resolvedErrorURL, "email_not_found");
|
|
141
132
|
}
|
|
142
133
|
const accountData = {
|
|
143
134
|
providerId: provider.id,
|
|
@@ -145,21 +136,27 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
145
136
|
...tokens,
|
|
146
137
|
scope: tokens.scopes?.join(",")
|
|
147
138
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
139
|
+
let result;
|
|
140
|
+
try {
|
|
141
|
+
result = await handleOAuthUserInfo(c, {
|
|
142
|
+
userInfo: {
|
|
143
|
+
...userInfo,
|
|
144
|
+
id: providerAccountId,
|
|
145
|
+
email: userInfo.email,
|
|
146
|
+
name: userInfo.name || ""
|
|
147
|
+
},
|
|
148
|
+
account: accountData,
|
|
149
|
+
callbackURL,
|
|
150
|
+
disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.options?.disableSignUp,
|
|
151
|
+
overrideUserInfo: provider.options?.overrideUserInfoOnSignIn
|
|
152
|
+
});
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (isAPIError(e) && e.body?.code) redirectOnError(c, resolvedErrorURL, e.body.code, e.body.message);
|
|
155
|
+
throw e;
|
|
156
|
+
}
|
|
160
157
|
if (result.error) {
|
|
161
158
|
c.context.logger.error(result.error.split(" ").join("_"));
|
|
162
|
-
|
|
159
|
+
redirectOnError(c, resolvedErrorURL, result.error.split(" ").join("_"));
|
|
163
160
|
}
|
|
164
161
|
const { session, user } = result.data;
|
|
165
162
|
await setSessionCookie(c, {
|
|
@@ -27,6 +27,7 @@ declare function sendVerificationEmailFn(ctx: GenericEndpointContext, user: User
|
|
|
27
27
|
declare const sendVerificationEmail: better_call0.StrictEndpoint<"/send-verification-email", {
|
|
28
28
|
method: "POST";
|
|
29
29
|
operationId: string;
|
|
30
|
+
cloneRequest: true;
|
|
30
31
|
body: z.ZodObject<{
|
|
31
32
|
email: z.ZodEmail;
|
|
32
33
|
callbackURL: z.ZodOptional<z.ZodString>;
|
|
@@ -31,11 +31,12 @@ async function sendVerificationEmailFn(ctx, user) {
|
|
|
31
31
|
user,
|
|
32
32
|
url,
|
|
33
33
|
token
|
|
34
|
-
}, ctx.request));
|
|
34
|
+
}, ctx.request?.clone()));
|
|
35
35
|
}
|
|
36
36
|
const sendVerificationEmail = createAuthEndpoint("/send-verification-email", {
|
|
37
37
|
method: "POST",
|
|
38
38
|
operationId: "sendVerificationEmail",
|
|
39
|
+
cloneRequest: true,
|
|
39
40
|
body: z.object({
|
|
40
41
|
email: z.email().meta({ description: "The email to send the verification email to" }),
|
|
41
42
|
callbackURL: z.string().meta({ description: "The URL to use for email verification callback" }).optional()
|
|
@@ -185,7 +186,7 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
185
186
|
},
|
|
186
187
|
url,
|
|
187
188
|
token: newToken
|
|
188
|
-
}, ctx.request));
|
|
189
|
+
}, ctx.request?.clone()));
|
|
189
190
|
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
190
191
|
return ctx.json({ status: true });
|
|
191
192
|
}
|
|
@@ -238,7 +239,7 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
238
239
|
user: updatedUser,
|
|
239
240
|
url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
|
|
240
241
|
token: newToken
|
|
241
|
-
}, ctx.request));
|
|
242
|
+
}, ctx.request?.clone()));
|
|
242
243
|
await setSessionCookie(ctx, {
|
|
243
244
|
session: activeSession.session,
|
|
244
245
|
user: {
|
|
@@ -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", {
|