better-auth 1.7.15 → 1.7.16
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 +2 -1
- package/dist/api/index.mjs +3 -2
- package/dist/api/routes/account.mjs +14 -6
- package/dist/api/routes/email-verification.mjs +29 -37
- package/dist/api/routes/sign-in.mjs +2 -2
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -4
- package/dist/api/state/session-source.d.mts +10 -0
- package/dist/api/state/session-source.mjs +28 -0
- package/dist/auth/trusted-origins.mjs +31 -8
- package/dist/client/index.d.mts +2 -1
- package/dist/db/internal-adapter.mjs +24 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +3 -5
- package/dist/oauth2/link-account.mjs +33 -3
- package/dist/oauth2/state.d.mts +6 -5
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +4 -3
- package/dist/plugins/admin/client.d.mts +5 -4
- package/dist/plugins/admin/error-codes.d.mts +1 -0
- package/dist/plugins/admin/error-codes.mjs +1 -0
- package/dist/plugins/admin/routes.mjs +8 -0
- package/dist/plugins/anonymous/index.mjs +2 -1
- package/dist/plugins/device-authorization/routes.mjs +3 -3
- package/dist/plugins/email-otp/routes.mjs +33 -50
- package/dist/plugins/generic-oauth/index.mjs +54 -30
- package/dist/plugins/index.d.mts +2 -1
- package/dist/plugins/index.mjs +2 -1
- package/dist/plugins/oauth-proxy/index.mjs +1 -1
- package/dist/plugins/one-tap/index.mjs +1 -1
- package/dist/plugins/organization/adapter.mjs +1 -0
- package/dist/plugins/organization/client.d.mts +1 -0
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +1 -0
- package/dist/plugins/organization/has-permission.d.mts +2 -3
- package/dist/plugins/organization/has-permission.mjs +62 -23
- package/dist/plugins/organization/index.d.mts +2 -1
- package/dist/plugins/organization/index.mjs +2 -1
- package/dist/plugins/organization/organization.d.mts +2 -2
- package/dist/plugins/organization/organization.mjs +1 -3
- package/dist/plugins/organization/permission.mjs +3 -3
- package/dist/plugins/organization/roles.d.mts +8 -0
- package/dist/plugins/organization/roles.mjs +26 -0
- package/dist/plugins/organization/routes/crud-access-control.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +22 -11
- package/dist/plugins/organization/routes/crud-members.mjs +10 -14
- package/dist/plugins/organization/routes/crud-org.mjs +12 -3
- package/dist/plugins/phone-number/client.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.mjs +2 -0
- package/dist/plugins/phone-number/index.d.mts +25 -1
- package/dist/plugins/phone-number/index.mjs +22 -6
- package/dist/plugins/phone-number/routes.mjs +34 -35
- package/dist/plugins/two-factor/index.d.mts +9 -2
- package/dist/plugins/two-factor/index.mjs +73 -26
- package/dist/plugins/two-factor/verify-two-factor.mjs +2 -2
- package/dist/plugins/username/index.mjs +1 -1
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +5 -2
- package/dist/utils/index.d.mts +2 -2
- package/package.json +9 -9
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { isAPIError } from "../utils/is-api-error.mjs";
|
|
2
2
|
import { parseAdditionalUserInputFromProviderProfile } from "../db/schema.mjs";
|
|
3
|
+
import { symmetricDecrypt } from "../crypto/index.mjs";
|
|
3
4
|
import { setAccountCookie } from "../cookies/session-store.mjs";
|
|
4
5
|
import { assertValidUserInfo } from "../utils/validate-user-info.mjs";
|
|
6
|
+
import { derivePurposeKey } from "../crypto/purpose.mjs";
|
|
5
7
|
import { OAUTH_CALLBACK_ERROR_CODES, redirectOnError } from "./errors.mjs";
|
|
8
|
+
import { isStateful } from "../api/routes/session.mjs";
|
|
9
|
+
import { toBoolean } from "../utils/boolean.mjs";
|
|
6
10
|
import { setTokenUtil } from "./utils.mjs";
|
|
7
11
|
import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
|
|
8
12
|
import { queueAfterTransactionHook, runWithTransaction } from "@better-auth/core/context";
|
|
@@ -19,7 +23,29 @@ function linkOAuthAccountFailure(code, message) {
|
|
|
19
23
|
}
|
|
20
24
|
};
|
|
21
25
|
}
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Whether the session that started a link is still live when the provider
|
|
28
|
+
* returns. Links whose state carries no session token (started before the
|
|
29
|
+
* token was recorded, or built by a direct caller) are not checked.
|
|
30
|
+
*/
|
|
31
|
+
async function isLinkSessionLive(c, link) {
|
|
32
|
+
if (!link.encryptedSessionToken || !isStateful(c)) return true;
|
|
33
|
+
const sessionToken = await symmetricDecrypt({
|
|
34
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-link-session-token"),
|
|
35
|
+
data: link.encryptedSessionToken
|
|
36
|
+
}).catch(() => null);
|
|
37
|
+
if (!sessionToken) return false;
|
|
38
|
+
const source = await c.context.internalAdapter.findSession(sessionToken);
|
|
39
|
+
if (!source) return false;
|
|
40
|
+
return source.session.userId === link.userId && source.session.expiresAt > /* @__PURE__ */ new Date();
|
|
41
|
+
}
|
|
42
|
+
async function linkOAuthAccount(c, opts) {
|
|
43
|
+
const { link, account, profile, scopes } = opts;
|
|
44
|
+
const userInfo = {
|
|
45
|
+
...opts.userInfo,
|
|
46
|
+
emailVerified: toBoolean(opts.userInfo.emailVerified)
|
|
47
|
+
};
|
|
48
|
+
if (!await isLinkSessionLive(c, link)) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
23
49
|
try {
|
|
24
50
|
await assertValidUserInfo(c, {
|
|
25
51
|
user: {
|
|
@@ -70,7 +96,11 @@ async function linkOAuthAccount(c, { link, userInfo, account, profile, scopes })
|
|
|
70
96
|
return { linked: true };
|
|
71
97
|
}
|
|
72
98
|
async function handleOAuthUserInfo(c, opts) {
|
|
73
|
-
const {
|
|
99
|
+
const { account, callbackURL, disableSignUp, overrideUserInfo } = opts;
|
|
100
|
+
const userInfo = {
|
|
101
|
+
...opts.userInfo,
|
|
102
|
+
emailVerified: toBoolean(opts.userInfo.emailVerified)
|
|
103
|
+
};
|
|
74
104
|
const source = opts.source ?? {
|
|
75
105
|
method: "oauth",
|
|
76
106
|
oauth: { providerId: account.providerId }
|
|
@@ -350,7 +380,7 @@ async function dispatchVerificationEmail(c, user, callbackURL, deferUntilAfterTr
|
|
|
350
380
|
if (!sendVerificationEmail) return;
|
|
351
381
|
const send = async () => {
|
|
352
382
|
try {
|
|
353
|
-
const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
|
|
383
|
+
const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
354
384
|
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
|
|
355
385
|
await c.context.runInBackgroundOrAwait(sendVerificationEmail({
|
|
356
386
|
user,
|
package/dist/oauth2/state.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StateData } from "../state.mjs";
|
|
1
2
|
import { GenericEndpointContext } from "@better-auth/core";
|
|
2
3
|
//#region src/oauth2/state.d.ts
|
|
3
4
|
/**
|
|
@@ -15,11 +16,10 @@ export declare function generateIdTokenNonce(provider: {
|
|
|
15
16
|
* by name instead of by position.
|
|
16
17
|
*/
|
|
17
18
|
export interface GenerateStateOptions {
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Link target when this flow links a provider identity to an existing user.
|
|
21
|
+
*/
|
|
22
|
+
link?: StateData["link"];
|
|
23
23
|
/** Extra data to round-trip through state; `false` writes none. */
|
|
24
24
|
additionalData?: Record<string, any> | false | undefined;
|
|
25
25
|
/** The `state` nonce already used to build the authorization URL. Minted when omitted. */
|
|
@@ -44,6 +44,7 @@ export declare function parseState(c: GenericEndpointContext): Promise<{
|
|
|
44
44
|
link?: {
|
|
45
45
|
email: string;
|
|
46
46
|
userId: string;
|
|
47
|
+
encryptedSessionToken?: string | undefined;
|
|
47
48
|
} | undefined;
|
|
48
49
|
requestSignUp?: boolean | undefined;
|
|
49
50
|
idTokenNonce?: string | undefined;
|
package/dist/package.mjs
CHANGED
|
@@ -2,49 +2,49 @@ import { ExactRoleStatements, Role, RoleInput, Statements } from "../../access/t
|
|
|
2
2
|
import "../../index.mjs";
|
|
3
3
|
//#region src/plugins/admin/access/statement.d.ts
|
|
4
4
|
export declare const defaultStatements: {
|
|
5
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
5
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
6
6
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
7
7
|
};
|
|
8
8
|
export declare const defaultAc: {
|
|
9
9
|
newRole<const TRoleStatements extends Statements>(statements: RoleInput<{
|
|
10
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
10
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
11
11
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
12
12
|
}, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, {
|
|
13
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
13
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
14
14
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
15
15
|
}>;
|
|
16
16
|
statements: {
|
|
17
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
17
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
18
18
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
21
|
export declare const adminAc: Role<ExactRoleStatements<{
|
|
22
|
-
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
|
|
22
|
+
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
23
23
|
readonly session: ["list", "revoke", "delete"];
|
|
24
24
|
}>, {
|
|
25
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
25
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
26
26
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
27
27
|
}>;
|
|
28
28
|
export declare const userAc: Role<ExactRoleStatements<{
|
|
29
29
|
readonly user: [];
|
|
30
30
|
readonly session: [];
|
|
31
31
|
}>, {
|
|
32
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
32
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
33
33
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
34
34
|
}>;
|
|
35
35
|
export declare const defaultRoles: {
|
|
36
36
|
admin: Role<ExactRoleStatements<{
|
|
37
|
-
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
|
|
37
|
+
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
38
38
|
readonly session: ["list", "revoke", "delete"];
|
|
39
39
|
}>, {
|
|
40
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
40
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
41
41
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
42
42
|
}>;
|
|
43
43
|
user: Role<ExactRoleStatements<{
|
|
44
44
|
readonly user: [];
|
|
45
45
|
readonly session: [];
|
|
46
46
|
}>, {
|
|
47
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
47
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
48
48
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
49
49
|
}>;
|
|
50
50
|
};
|
|
@@ -11,6 +11,7 @@ const defaultStatements = {
|
|
|
11
11
|
"delete",
|
|
12
12
|
"set-password",
|
|
13
13
|
"set-email",
|
|
14
|
+
"set-phone",
|
|
14
15
|
"get",
|
|
15
16
|
"update"
|
|
16
17
|
],
|
|
@@ -31,6 +32,7 @@ const adminAc = defaultAc.newRole({
|
|
|
31
32
|
"delete",
|
|
32
33
|
"set-password",
|
|
33
34
|
"set-email",
|
|
35
|
+
"set-phone",
|
|
34
36
|
"get",
|
|
35
37
|
"update"
|
|
36
38
|
],
|
|
@@ -819,13 +819,13 @@ export declare const admin: <O extends AdminOptions>(options?: O | undefined) =>
|
|
|
819
819
|
$Infer: {
|
|
820
820
|
body: {
|
|
821
821
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
822
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
822
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
823
823
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
824
824
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
825
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
825
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
826
826
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
827
827
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
828
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
828
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
829
829
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
830
830
|
})[key]> : never)[] | undefined; };
|
|
831
831
|
} & {
|
|
@@ -862,6 +862,7 @@ export declare const admin: <O extends AdminOptions>(options?: O | undefined) =>
|
|
|
862
862
|
YOU_CANNOT_IMPERSONATE_ADMINS: import("@better-auth/core/utils/error-codes").RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
863
863
|
INVALID_ROLE_TYPE: import("@better-auth/core/utils/error-codes").RawError<"INVALID_ROLE_TYPE">;
|
|
864
864
|
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
865
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER">;
|
|
865
866
|
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: import("@better-auth/core/utils/error-codes").RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
866
867
|
USER_ID_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"USER_ID_CANNOT_BE_UPDATED">;
|
|
867
868
|
};
|
|
@@ -14,7 +14,7 @@ export declare const adminClient: <O extends AdminClientOptions>(options?: O | u
|
|
|
14
14
|
version: string;
|
|
15
15
|
$InferServerPlugin: ReturnType<typeof admin<{
|
|
16
16
|
ac: O["ac"] extends AccessControl ? O["ac"] : AccessControl<{
|
|
17
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
17
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
18
18
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
19
19
|
}>;
|
|
20
20
|
roles: O["roles"] extends Record<string, Role> ? O["roles"] : {
|
|
@@ -28,13 +28,13 @@ export declare const adminClient: <O extends AdminClientOptions>(options?: O | u
|
|
|
28
28
|
roles: any;
|
|
29
29
|
} ? keyof O["roles"] : "admin" | "user">(data: {
|
|
30
30
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
31
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
31
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
32
32
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
33
33
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
34
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
34
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
35
35
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
36
36
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
37
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
37
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "set-phone", "get", "update"];
|
|
38
38
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
39
39
|
})[key]> : never)[] | undefined; };
|
|
40
40
|
} & {
|
|
@@ -74,6 +74,7 @@ export declare const adminClient: <O extends AdminClientOptions>(options?: O | u
|
|
|
74
74
|
YOU_CANNOT_IMPERSONATE_ADMINS: import("@better-auth/core/utils/error-codes").RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
75
75
|
INVALID_ROLE_TYPE: import("@better-auth/core/utils/error-codes").RawError<"INVALID_ROLE_TYPE">;
|
|
76
76
|
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
77
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER">;
|
|
77
78
|
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: import("@better-auth/core/utils/error-codes").RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
78
79
|
USER_ID_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"USER_ID_CANNOT_BE_UPDATED">;
|
|
79
80
|
};
|
|
@@ -22,6 +22,7 @@ export declare const ADMIN_ERROR_CODES: {
|
|
|
22
22
|
YOU_CANNOT_IMPERSONATE_ADMINS: import("@better-auth/core/utils/error-codes").RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
23
23
|
INVALID_ROLE_TYPE: import("@better-auth/core/utils/error-codes").RawError<"INVALID_ROLE_TYPE">;
|
|
24
24
|
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
25
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER: import("@better-auth/core/utils/error-codes").RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER">;
|
|
25
26
|
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: import("@better-auth/core/utils/error-codes").RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
26
27
|
USER_ID_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"USER_ID_CANNOT_BE_UPDATED">;
|
|
27
28
|
};
|
|
@@ -23,6 +23,7 @@ const ADMIN_ERROR_CODES = defineErrorCodes({
|
|
|
23
23
|
YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins",
|
|
24
24
|
INVALID_ROLE_TYPE: "Invalid role type",
|
|
25
25
|
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: "You are not allowed to update users email",
|
|
26
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER: "You are not allowed to update users phone number",
|
|
26
27
|
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: "Password cannot be updated through update-user. Use the set-user-password endpoint instead",
|
|
27
28
|
USER_ID_CANNOT_BE_UPDATED: "User id cannot be updated"
|
|
28
29
|
});
|
|
@@ -301,6 +301,14 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
301
301
|
updateData.email = email;
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
|
+
if (hasDataKey("phoneNumber") || hasDataKey("phoneNumberVerified")) {
|
|
305
|
+
if (!hasPermission({
|
|
306
|
+
userId: ctx.context.session.user.id,
|
|
307
|
+
role: ctx.context.session.user.role,
|
|
308
|
+
options: opts,
|
|
309
|
+
permissions: { user: ["set-phone"] }
|
|
310
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PHONE_NUMBER);
|
|
311
|
+
}
|
|
304
312
|
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
305
313
|
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
|
|
306
314
|
if (updateData.banned === true) await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mergeSchema, parseUserOutput } from "../../db/schema.mjs";
|
|
2
2
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
3
3
|
import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
4
|
-
import { addOAuthServerContext, getOAuthState } from "../../api/state/oauth.mjs";
|
|
5
4
|
import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
|
|
5
|
+
import { addOAuthServerContext, getOAuthState } from "../../api/state/oauth.mjs";
|
|
6
6
|
import { APIError } from "../../api/index.mjs";
|
|
7
7
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
8
8
|
import { ANONYMOUS_ERROR_CODES } from "./error-codes.mjs";
|
|
@@ -187,6 +187,7 @@ const anonymous = (options) => {
|
|
|
187
187
|
if (ctx.path === "/sign-in/anonymous" && !ctx.context.newSession) throw APIError.from("BAD_REQUEST", ANONYMOUS_ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY);
|
|
188
188
|
const newSession = ctx.context.newSession;
|
|
189
189
|
if (!newSession) return;
|
|
190
|
+
if (await ctx.context.getPlugin("two-factor")?.shouldRequireTwoFactor(ctx)) return;
|
|
190
191
|
if (options?.onLinkAccount) await options.onLinkAccount({
|
|
191
192
|
anonymousUser: {
|
|
192
193
|
session: session.session,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ms } from "../../utils/time.mjs";
|
|
2
2
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
3
|
-
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
3
|
+
import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
4
4
|
import { DEVICE_AUTHORIZATION_ERROR_CODES } from "./error-codes.mjs";
|
|
5
5
|
import { APIError } from "@better-auth/core/error";
|
|
6
6
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -526,7 +526,7 @@ const deviceVerify = (grant) => createAuthEndpoint("/device", {
|
|
|
526
526
|
error: "expired_token",
|
|
527
527
|
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message
|
|
528
528
|
});
|
|
529
|
-
const session = await
|
|
529
|
+
const session = await getAuthoritativeSessionFromCtx(ctx);
|
|
530
530
|
if (session?.user?.id && !deviceCodeRecord.userId && deviceCodeRecord.status === "pending") {
|
|
531
531
|
if (await ctx.context.adapter.incrementOne({
|
|
532
532
|
model: "deviceCode",
|
|
@@ -587,7 +587,7 @@ const deviceApprove = createAuthEndpoint("/device/approve", {
|
|
|
587
587
|
} }
|
|
588
588
|
} }
|
|
589
589
|
}, async (ctx) => {
|
|
590
|
-
const session = await
|
|
590
|
+
const session = await getAuthoritativeSessionFromCtx(ctx);
|
|
591
591
|
if (!session) throw new APIError("UNAUTHORIZED", {
|
|
592
592
|
error: "unauthorized",
|
|
593
593
|
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED.message
|
|
@@ -101,12 +101,13 @@ const sendVerificationOTP = (opts) => createAuthEndpoint("/email-otp/send-verifi
|
|
|
101
101
|
const identifier = toOTPIdentifier(ctx.body.type, email);
|
|
102
102
|
const otp = await resolveOTP(ctx, opts, email, ctx.body.type);
|
|
103
103
|
const shouldSendOTP = ctx.body.type === "sign-in" && !opts.disableSignUp;
|
|
104
|
-
|
|
104
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
105
|
+
if (!user && !shouldSendOTP) {
|
|
105
106
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
106
107
|
return ctx.json({ success: true });
|
|
107
108
|
}
|
|
108
109
|
await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
|
|
109
|
-
email,
|
|
110
|
+
email: user?.user.email ?? email,
|
|
110
111
|
otp,
|
|
111
112
|
type: ctx.body.type
|
|
112
113
|
}, ctx));
|
|
@@ -236,23 +237,7 @@ const checkVerificationOTP = (opts) => createAuthEndpoint("/email-otp/check-veri
|
|
|
236
237
|
}, async (ctx) => {
|
|
237
238
|
const email = ctx.body.email.toLowerCase();
|
|
238
239
|
if (!z.email().safeParse(email).success) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
239
|
-
|
|
240
|
-
const verificationValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
241
|
-
if (!verificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
242
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
|
|
243
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
244
|
-
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
|
|
245
|
-
}
|
|
246
|
-
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
|
|
247
|
-
const allowedAttempts = opts?.allowedAttempts || 3;
|
|
248
|
-
if (attempts && parseInt(attempts) >= allowedAttempts) {
|
|
249
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
250
|
-
throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
|
|
251
|
-
}
|
|
252
|
-
if (!await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp)) {
|
|
253
|
-
await ctx.context.internalAdapter.updateVerificationByIdentifier(identifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
|
|
254
|
-
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
255
|
-
}
|
|
240
|
+
await atomicVerifyOTP(ctx, opts, toOTPIdentifier(ctx.body.type, email), ctx.body.otp, "restore");
|
|
256
241
|
if (!await ctx.context.internalAdapter.findUserByEmail(email))
|
|
257
242
|
/**
|
|
258
243
|
* safe to leak the existence of a user, given the user has already the OTP from the
|
|
@@ -480,14 +465,16 @@ const requestPasswordResetEmailOTP = (opts) => createAuthEndpoint("/email-otp/re
|
|
|
480
465
|
} }
|
|
481
466
|
}, async (ctx) => {
|
|
482
467
|
const email = ctx.body.email.toLowerCase();
|
|
468
|
+
if (!z.email().safeParse(email).success) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
483
469
|
const identifier = toOTPIdentifier("forget-password", email);
|
|
484
470
|
const otp = await resolveOTP(ctx, opts, email, "forget-password");
|
|
485
|
-
|
|
471
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
472
|
+
if (!user) {
|
|
486
473
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
487
474
|
return ctx.json({ success: true });
|
|
488
475
|
}
|
|
489
476
|
await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
|
|
490
|
-
email,
|
|
477
|
+
email: user.user.email,
|
|
491
478
|
otp,
|
|
492
479
|
type: "forget-password"
|
|
493
480
|
}, ctx));
|
|
@@ -532,14 +519,16 @@ const forgetPasswordEmailOTP = (opts) => {
|
|
|
532
519
|
}, async (ctx) => {
|
|
533
520
|
warnDeprecation();
|
|
534
521
|
const email = ctx.body.email.toLowerCase();
|
|
522
|
+
if (!z.email().safeParse(email).success) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
535
523
|
const identifier = toOTPIdentifier("forget-password", email);
|
|
536
524
|
const otp = await resolveOTP(ctx, opts, email, "forget-password");
|
|
537
|
-
|
|
525
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
526
|
+
if (!user) {
|
|
538
527
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
539
528
|
return ctx.json({ success: true });
|
|
540
529
|
}
|
|
541
530
|
await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
|
|
542
|
-
email,
|
|
531
|
+
email: user.user.email,
|
|
543
532
|
otp,
|
|
544
533
|
type: "forget-password"
|
|
545
534
|
}, ctx));
|
|
@@ -582,18 +571,15 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
|
|
|
582
571
|
} }
|
|
583
572
|
}, async (ctx) => {
|
|
584
573
|
const email = ctx.body.email.toLowerCase();
|
|
574
|
+
if (!z.email().safeParse(email).success) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
585
575
|
assertPasswordNotTooShort(ctx, ctx.body.password);
|
|
586
576
|
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
587
577
|
const identifier = toOTPIdentifier("forget-password", email);
|
|
588
|
-
|
|
589
|
-
if (!await matchesUsableStoredOTP(ctx, opts, identifier, ctx.body.otp)) {
|
|
590
|
-
await atomicVerifyOTP(ctx, opts, identifier, ctx.body.otp);
|
|
591
|
-
otpConsumed = true;
|
|
592
|
-
}
|
|
578
|
+
await atomicVerifyOTP(ctx, opts, identifier, ctx.body.otp, "restore");
|
|
593
579
|
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
594
580
|
if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
595
581
|
const passwordHash = await ctx.context.password.hash(ctx.body.password);
|
|
596
|
-
await resetCredentialPassword(ctx, user.user.id, passwordHash,
|
|
582
|
+
await resetCredentialPassword(ctx, user.user.id, passwordHash, () => atomicVerifyOTP(ctx, opts, identifier, ctx.body.otp));
|
|
597
583
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user: user.user }, ctx.request);
|
|
598
584
|
if (!user.user.emailVerified) await ctx.context.internalAdapter.updateUser(user.user.id, { emailVerified: true });
|
|
599
585
|
return ctx.json({ success: true });
|
|
@@ -653,13 +639,14 @@ const requestEmailChangeEmailOTP = (opts) => createAuthEndpoint("/email-otp/requ
|
|
|
653
639
|
type: "change-email"
|
|
654
640
|
}, ctx) || defaultOTPGenerator(opts);
|
|
655
641
|
const storedOTP = await storeOTP(ctx, opts, otp);
|
|
642
|
+
const identifier = toOTPIdentifier("change-email", `${ctx.context.session.user.id}:${newEmail}`);
|
|
656
643
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
657
644
|
value: `${storedOTP}:0`,
|
|
658
|
-
identifier
|
|
645
|
+
identifier,
|
|
659
646
|
expiresAt: getDate(opts.expiresIn, "sec")
|
|
660
647
|
});
|
|
661
648
|
if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) {
|
|
662
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
|
|
649
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
663
650
|
return ctx.json({ success: true });
|
|
664
651
|
}
|
|
665
652
|
await ctx.context.runInBackgroundOrAwait(opts.sendVerificationOTP({
|
|
@@ -716,7 +703,7 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
|
|
|
716
703
|
ctx.context.logger.error("Email is the same");
|
|
717
704
|
throw APIError$1.fromStatus("BAD_REQUEST", { message: "Email is the same" });
|
|
718
705
|
}
|
|
719
|
-
await atomicVerifyOTP(ctx, opts, toOTPIdentifier("change-email", `${
|
|
706
|
+
await atomicVerifyOTP(ctx, opts, toOTPIdentifier("change-email", `${session.user.id}:${newEmail}`), ctx.body.otp);
|
|
720
707
|
const currentUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
721
708
|
if (!currentUser)
|
|
722
709
|
/**
|
|
@@ -746,28 +733,17 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
|
|
|
746
733
|
});
|
|
747
734
|
const defaultOTPGenerator = (options) => generateRandomString(options.otpLength ?? 6, "0-9");
|
|
748
735
|
/**
|
|
749
|
-
*
|
|
750
|
-
* attempts left, without consuming it or counting an attempt.
|
|
751
|
-
*/
|
|
752
|
-
async function matchesUsableStoredOTP(ctx, opts, identifier, providedOTP) {
|
|
753
|
-
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
754
|
-
if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return false;
|
|
755
|
-
const [otpValue, attempts] = splitAtLastColon(existing.value);
|
|
756
|
-
if (parseInt(attempts || "0") >= (opts?.allowedAttempts || 3)) return false;
|
|
757
|
-
return verifyStoredOTP(ctx, opts, otpValue, providedOTP);
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Verifies a single-use OTP with race-condition protection.
|
|
736
|
+
* Verifies an OTP with race-condition protection.
|
|
761
737
|
*
|
|
762
738
|
* The atomic consume is the single gate: only the first concurrent caller
|
|
763
739
|
* receives the record, every later racer receives `null` and is rejected, so
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
740
|
+
* only one caller at a time can compare a guess. Credential routes leave a
|
|
741
|
+
* successful OTP consumed. The optional preflight restores it unchanged so
|
|
742
|
+
* the documented follow-up call can use it. A wrong guess restores it with
|
|
743
|
+
* one additional attempt and the original expiry. An exhausted record stays
|
|
744
|
+
* consumed, locking the identifier out.
|
|
769
745
|
*/
|
|
770
|
-
async function atomicVerifyOTP(ctx, opts, identifier, providedOTP) {
|
|
746
|
+
async function atomicVerifyOTP(ctx, opts, identifier, providedOTP, successDisposition = "consume") {
|
|
771
747
|
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
772
748
|
if (existing && existing.expiresAt < /* @__PURE__ */ new Date()) {
|
|
773
749
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
@@ -787,6 +763,13 @@ async function atomicVerifyOTP(ctx, opts, identifier, providedOTP) {
|
|
|
787
763
|
});
|
|
788
764
|
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
789
765
|
}
|
|
766
|
+
if (successDisposition === "consume") return;
|
|
767
|
+
if (await ctx.context.internalAdapter.findVerificationValue(identifier)) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
768
|
+
if (!await ctx.context.internalAdapter.createVerificationValue({
|
|
769
|
+
value: consumed.value,
|
|
770
|
+
identifier,
|
|
771
|
+
expiresAt: consumed.expiresAt
|
|
772
|
+
})) throw APIError$1.fromStatus("INTERNAL_SERVER_ERROR", { message: "Unable to complete OTP verification" });
|
|
790
773
|
}
|
|
791
774
|
//#endregion
|
|
792
775
|
export { changeEmailEmailOTP, checkVerificationOTP, createVerificationOTP, forgetPasswordEmailOTP, getVerificationOTP, requestEmailChangeEmailOTP, requestPasswordResetEmailOTP, resetPasswordEmailOTP, sendVerificationOTP, signInEmailOTP, verifyEmailOTP };
|