better-auth 1.6.1 → 1.6.3
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 +0 -2
- package/dist/api/routes/account.mjs +1 -1
- package/dist/api/routes/callback.mjs +2 -2
- package/dist/api/routes/email-verification.mjs +1 -1
- package/dist/api/routes/password.mjs +1 -1
- package/dist/api/routes/session.d.mts +0 -1
- package/dist/api/routes/session.mjs +3 -4
- package/dist/api/routes/sign-in.mjs +1 -1
- package/dist/api/to-auth-endpoints.mjs +27 -3
- package/dist/auth/base.mjs +5 -24
- package/dist/client/plugins/index.d.mts +2 -2
- package/dist/client/query.mjs +4 -4
- package/dist/context/create-context.mjs +2 -2
- package/dist/context/helpers.mjs +61 -3
- package/dist/cookies/index.mjs +3 -3
- package/dist/crypto/index.mjs +2 -2
- package/dist/db/index.mjs +1 -1
- package/dist/db/internal-adapter.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/integrations/next-js.mjs +21 -12
- package/dist/oauth2/state.d.mts +1 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/admin.d.mts +1 -3
- package/dist/plugins/admin/routes.mjs +2 -8
- package/dist/plugins/device-authorization/routes.mjs +1 -1
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/jwt/utils.mjs +1 -1
- package/dist/plugins/mcp/index.mjs +20 -8
- package/dist/plugins/oauth-proxy/index.mjs +5 -1
- package/dist/plugins/oidc-provider/index.mjs +2 -2
- package/dist/plugins/organization/organization.d.mts +1 -3
- package/dist/plugins/organization/organization.mjs +1 -7
- package/dist/plugins/organization/routes/crud-invites.mjs +1 -1
- package/dist/plugins/organization/routes/crud-org.mjs +1 -1
- package/dist/plugins/organization/routes/crud-team.mjs +1 -1
- package/dist/plugins/phone-number/routes.mjs +1 -1
- package/dist/plugins/two-factor/backup-codes/index.d.mts +2 -1
- package/dist/plugins/two-factor/backup-codes/index.mjs +12 -17
- package/dist/plugins/two-factor/client.d.mts +12 -2
- package/dist/plugins/two-factor/client.mjs +1 -1
- package/dist/plugins/two-factor/index.d.mts +9 -2
- package/dist/plugins/two-factor/index.mjs +35 -3
- package/dist/plugins/two-factor/otp/index.mjs +1 -1
- package/dist/plugins/two-factor/schema.d.mts +6 -0
- package/dist/plugins/two-factor/schema.mjs +6 -0
- package/dist/plugins/two-factor/totp/index.mjs +19 -10
- package/dist/plugins/two-factor/types.d.mts +1 -1
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +18 -2
- package/dist/test-utils/test-instance.d.mts +0 -6
- package/dist/test-utils/test-instance.mjs +7 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +22 -15
- package/dist/utils/url.mjs +54 -28
- package/package.json +9 -9
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getBaseURL, isDynamicBaseURLConfig, resolveBaseURL } from "../../utils/url.mjs";
|
|
2
|
-
import { generateRandomString } from "../../crypto/random.mjs";
|
|
3
2
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
3
|
+
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
4
|
import { expireCookie } from "../../cookies/index.mjs";
|
|
5
|
+
import { resolveDynamicTrustedProxyHeaders } from "../../context/helpers.mjs";
|
|
5
6
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
7
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
8
|
import { APIError } from "../../api/index.mjs";
|
|
@@ -15,9 +16,9 @@ import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
|
15
16
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
16
17
|
import * as z from "zod";
|
|
17
18
|
import { base64 } from "@better-auth/utils/base64";
|
|
18
|
-
import { createHash } from "@better-auth/utils/hash";
|
|
19
|
-
import { getWebcryptoSubtle } from "@better-auth/utils";
|
|
20
19
|
import { SignJWT } from "jose";
|
|
20
|
+
import { getWebcryptoSubtle } from "@better-auth/utils";
|
|
21
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
21
22
|
//#region src/plugins/mcp/index.ts
|
|
22
23
|
const getMCPProviderMetadata = (ctx, options) => {
|
|
23
24
|
const issuer = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : "";
|
|
@@ -662,10 +663,15 @@ const mcp = (options) => {
|
|
|
662
663
|
const withMcpAuth = (auth, handler) => {
|
|
663
664
|
return async (req) => {
|
|
664
665
|
const basePath = auth.options.basePath || "/api/auth";
|
|
665
|
-
const
|
|
666
|
+
const trustedProxyHeaders = resolveDynamicTrustedProxyHeaders(auth.options);
|
|
667
|
+
const baseURL = isDynamicBaseURLConfig(auth.options.baseURL) ? resolveBaseURL(auth.options.baseURL, basePath, req, void 0, trustedProxyHeaders) : getBaseURL(typeof auth.options.baseURL === "string" ? auth.options.baseURL : void 0, basePath);
|
|
666
668
|
if (!baseURL && !isProduction) logger.warn("Unable to get the baseURL, please check your config!");
|
|
667
|
-
const session = await auth.api.getMcpSession({
|
|
668
|
-
|
|
669
|
+
const session = await auth.api.getMcpSession({
|
|
670
|
+
request: req,
|
|
671
|
+
headers: req.headers,
|
|
672
|
+
asResponse: false
|
|
673
|
+
});
|
|
674
|
+
const wwwAuthenticateValue = baseURL ? `Bearer resource_metadata="${baseURL}/.well-known/oauth-protected-resource"` : "Bearer";
|
|
669
675
|
if (!session) return Response.json({
|
|
670
676
|
jsonrpc: "2.0",
|
|
671
677
|
error: {
|
|
@@ -686,7 +692,10 @@ const withMcpAuth = (auth, handler) => {
|
|
|
686
692
|
};
|
|
687
693
|
const oAuthDiscoveryMetadata = (auth) => {
|
|
688
694
|
return async (request) => {
|
|
689
|
-
const res = await auth.api.getMcpOAuthConfig(
|
|
695
|
+
const res = await auth.api.getMcpOAuthConfig({
|
|
696
|
+
request,
|
|
697
|
+
asResponse: false
|
|
698
|
+
});
|
|
690
699
|
return new Response(JSON.stringify(res), {
|
|
691
700
|
status: 200,
|
|
692
701
|
headers: {
|
|
@@ -701,7 +710,10 @@ const oAuthDiscoveryMetadata = (auth) => {
|
|
|
701
710
|
};
|
|
702
711
|
const oAuthProtectedResourceMetadata = (auth) => {
|
|
703
712
|
return async (request) => {
|
|
704
|
-
const res = await auth.api.getMCPProtectedResource(
|
|
713
|
+
const res = await auth.api.getMCPProtectedResource({
|
|
714
|
+
request,
|
|
715
|
+
asResponse: false
|
|
716
|
+
});
|
|
705
717
|
return new Response(JSON.stringify(res), {
|
|
706
718
|
status: 200,
|
|
707
719
|
headers: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getOrigin } from "../../utils/url.mjs";
|
|
2
2
|
import { originCheck } from "../../api/middlewares/origin-check.mjs";
|
|
3
|
-
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
4
3
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
4
|
+
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
5
5
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
6
6
|
import { parseGenericState } from "../../state.mjs";
|
|
7
7
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
@@ -164,6 +164,10 @@ const oAuthProxy = (opts) => {
|
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
const errorURL = stateData.errorURL || ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
167
|
+
if (stateData.oauthState !== void 0 && stateData.oauthState !== statePackage.state) {
|
|
168
|
+
ctx.context.logger.error("OAuth proxy state binding mismatch");
|
|
169
|
+
throw redirectOnError(ctx, errorURL, "state_mismatch");
|
|
170
|
+
}
|
|
167
171
|
if (error) throw redirectOnError(ctx, errorURL, error);
|
|
168
172
|
if (!code) {
|
|
169
173
|
ctx.context.logger.error("OAuth callback missing authorization code");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mergeSchema } from "../../db/schema.mjs";
|
|
2
|
+
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
2
3
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
3
4
|
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
4
|
-
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
5
5
|
import { expireCookie } from "../../cookies/index.mjs";
|
|
6
6
|
import { getSessionFromCtx, sessionMiddleware } from "../../api/routes/session.mjs";
|
|
7
7
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
@@ -18,8 +18,8 @@ import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api"
|
|
|
18
18
|
import { deprecate } from "@better-auth/core/utils/deprecate";
|
|
19
19
|
import * as z from "zod";
|
|
20
20
|
import { base64 } from "@better-auth/utils/base64";
|
|
21
|
-
import { createHash } from "@better-auth/utils/hash";
|
|
22
21
|
import { SignJWT, jwtVerify } from "jose";
|
|
22
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
23
23
|
//#region src/plugins/oidc-provider/index.ts
|
|
24
24
|
/**
|
|
25
25
|
* Get a client by ID, checking trusted clients first, then database
|
|
@@ -99,11 +99,9 @@ declare const createHasPermission: <O extends OrganizationOptions>(options: O) =
|
|
|
99
99
|
requireHeaders: true;
|
|
100
100
|
body: z.ZodIntersection<z.ZodObject<{
|
|
101
101
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
102
|
-
}, z.core.$strip>, z.
|
|
102
|
+
}, z.core.$strip>, z.ZodXor<readonly [z.ZodObject<{
|
|
103
103
|
permission: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
104
|
-
permissions: z.ZodUndefined;
|
|
105
104
|
}, z.core.$strip>, z.ZodObject<{
|
|
106
|
-
permission: z.ZodUndefined;
|
|
107
105
|
permissions: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
108
106
|
}, z.core.$strip>]>>;
|
|
109
107
|
use: ((inputContext: better_call0.MiddlewareInputContext<{
|
|
@@ -18,13 +18,7 @@ import * as z from "zod";
|
|
|
18
18
|
function parseRoles(roles) {
|
|
19
19
|
return Array.isArray(roles) ? roles.join(",") : roles;
|
|
20
20
|
}
|
|
21
|
-
const createHasPermissionBodySchema = z.object({ organizationId: z.string().optional() }).and(z.
|
|
22
|
-
permission: z.record(z.string(), z.array(z.string())),
|
|
23
|
-
permissions: z.undefined()
|
|
24
|
-
}), z.object({
|
|
25
|
-
permission: z.undefined(),
|
|
26
|
-
permissions: z.record(z.string(), z.array(z.string()))
|
|
27
|
-
})]));
|
|
21
|
+
const createHasPermissionBodySchema = z.object({ organizationId: z.string().optional() }).and(z.xor([z.object({ permission: z.record(z.string(), z.array(z.string())) }), z.object({ permissions: z.record(z.string(), z.array(z.string())) })]));
|
|
28
22
|
const createHasPermission = (options) => {
|
|
29
23
|
return createAuthEndpoint("/organization/has-permission", {
|
|
30
24
|
method: "POST",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getDate } from "../../../utils/date.mjs";
|
|
2
|
-
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
3
2
|
import { setSessionCookie } from "../../../cookies/index.mjs";
|
|
3
|
+
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
4
4
|
import { getSessionFromCtx } from "../../../api/routes/session.mjs";
|
|
5
5
|
import { defaultRoles } from "../access/statement.mjs";
|
|
6
6
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
2
1
|
import { setSessionCookie } from "../../../cookies/index.mjs";
|
|
2
|
+
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
3
3
|
import { getSessionFromCtx, requestOnlySessionMiddleware } from "../../../api/routes/session.mjs";
|
|
4
4
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
5
5
|
import { getOrgAdapter } from "../adapter.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
2
1
|
import { setSessionCookie } from "../../../cookies/index.mjs";
|
|
2
|
+
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
3
3
|
import { getSessionFromCtx } from "../../../api/routes/session.mjs";
|
|
4
4
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
5
5
|
import { getOrgAdapter } from "../adapter.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getDate } from "../../utils/date.mjs";
|
|
2
1
|
import { parseUserInput, parseUserOutput } from "../../db/schema.mjs";
|
|
2
|
+
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
@@ -36,6 +36,7 @@ interface BackupCodeOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
allowPasswordless?: boolean | undefined;
|
|
38
38
|
}
|
|
39
|
+
declare function encodeBackupCodes(codes: string[], secret: string | SecretConfig, options?: BackupCodeOptions | undefined): Promise<string>;
|
|
39
40
|
declare function generateBackupCodes(secret: string | SecretConfig, options?: BackupCodeOptions | undefined): Promise<{
|
|
40
41
|
backupCodes: string[];
|
|
41
42
|
encryptedBackupCodes: string;
|
|
@@ -286,4 +287,4 @@ declare const backupCode2fa: (opts: BackupCodeOptions) => {
|
|
|
286
287
|
};
|
|
287
288
|
};
|
|
288
289
|
//#endregion
|
|
289
|
-
export { BackupCodeOptions, backupCode2fa, generateBackupCodes, getBackupCodes, verifyBackupCode };
|
|
290
|
+
export { BackupCodeOptions, backupCode2fa, encodeBackupCodes, generateBackupCodes, getBackupCodes, verifyBackupCode };
|
|
@@ -14,22 +14,20 @@ import * as z from "zod";
|
|
|
14
14
|
function generateBackupCodesFn(options) {
|
|
15
15
|
return Array.from({ length: options?.amount ?? 10 }).fill(null).map(() => generateRandomString(options?.length ?? 10, "a-z", "0-9", "A-Z")).map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
|
|
16
16
|
}
|
|
17
|
+
async function encodeBackupCodes(codes, secret, options) {
|
|
18
|
+
const json = JSON.stringify(codes);
|
|
19
|
+
if (options?.storeBackupCodes === "encrypted") return symmetricEncrypt({
|
|
20
|
+
data: json,
|
|
21
|
+
key: secret
|
|
22
|
+
});
|
|
23
|
+
if (typeof options?.storeBackupCodes === "object" && "encrypt" in options?.storeBackupCodes) return options.storeBackupCodes.encrypt(json);
|
|
24
|
+
return json;
|
|
25
|
+
}
|
|
17
26
|
async function generateBackupCodes(secret, options) {
|
|
18
27
|
const backupCodes = options?.customBackupCodesGenerate ? options.customBackupCodesGenerate() : generateBackupCodesFn(options);
|
|
19
|
-
if (options?.storeBackupCodes === "encrypted") return {
|
|
20
|
-
backupCodes,
|
|
21
|
-
encryptedBackupCodes: await symmetricEncrypt({
|
|
22
|
-
data: JSON.stringify(backupCodes),
|
|
23
|
-
key: secret
|
|
24
|
-
})
|
|
25
|
-
};
|
|
26
|
-
if (typeof options?.storeBackupCodes === "object" && "encrypt" in options?.storeBackupCodes) return {
|
|
27
|
-
backupCodes,
|
|
28
|
-
encryptedBackupCodes: await options?.storeBackupCodes.encrypt(JSON.stringify(backupCodes))
|
|
29
|
-
};
|
|
30
28
|
return {
|
|
31
29
|
backupCodes,
|
|
32
|
-
encryptedBackupCodes:
|
|
30
|
+
encryptedBackupCodes: await encodeBackupCodes(backupCodes, secret, options)
|
|
33
31
|
};
|
|
34
32
|
}
|
|
35
33
|
async function verifyBackupCode(data, key, options) {
|
|
@@ -177,11 +175,8 @@ const backupCode2fa = (opts) => {
|
|
|
177
175
|
backupCodes: twoFactor.backupCodes,
|
|
178
176
|
code: ctx.body.code
|
|
179
177
|
}, ctx.context.secretConfig, opts);
|
|
180
|
-
if (!validate.status) throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE);
|
|
181
|
-
const updatedBackupCodes = await
|
|
182
|
-
key: ctx.context.secretConfig,
|
|
183
|
-
data: JSON.stringify(validate.updated)
|
|
184
|
-
});
|
|
178
|
+
if (!validate.status || !validate.updated) throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE);
|
|
179
|
+
const updatedBackupCodes = await encodeBackupCodes(validate.updated, ctx.context.secretConfig, opts);
|
|
185
180
|
if (!await ctx.context.adapter.update({
|
|
186
181
|
model: twoFactorTable,
|
|
187
182
|
update: { backupCodes: updatedBackupCodes },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BackupCodeOptions, backupCode2fa, generateBackupCodes, getBackupCodes, verifyBackupCode } from "./backup-codes/index.mjs";
|
|
1
|
+
import { BackupCodeOptions, backupCode2fa, encodeBackupCodes, generateBackupCodes, getBackupCodes, verifyBackupCode } from "./backup-codes/index.mjs";
|
|
2
2
|
import { OTPOptions, otp2fa } from "./otp/index.mjs";
|
|
3
3
|
import { TOTPOptions, totp2fa } from "./totp/index.mjs";
|
|
4
4
|
import { TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor } from "./types.mjs";
|
|
@@ -19,8 +19,18 @@ declare const twoFactorClient: (options?: {
|
|
|
19
19
|
/**
|
|
20
20
|
* a redirect function to call if a user needs to verify
|
|
21
21
|
* their two factor
|
|
22
|
+
*
|
|
23
|
+
* @param context.twoFactorMethods - The list of
|
|
24
|
+
* enabled two factor providers (e.g. ["totp", "otp"]).
|
|
25
|
+
* Use this to determine which 2FA UI to show.
|
|
22
26
|
*/
|
|
23
|
-
onTwoFactorRedirect?: (
|
|
27
|
+
onTwoFactorRedirect?: (context: {
|
|
28
|
+
/**
|
|
29
|
+
* The list of enabled two factor providers
|
|
30
|
+
* for the user (e.g. ["totp", "otp"]).
|
|
31
|
+
*/
|
|
32
|
+
twoFactorMethods?: string[];
|
|
33
|
+
}) => void | Promise<void>;
|
|
24
34
|
} | undefined) => {
|
|
25
35
|
id: "two-factor";
|
|
26
36
|
version: string;
|
|
@@ -26,7 +26,7 @@ const twoFactorClient = (options) => {
|
|
|
26
26
|
hooks: { async onSuccess(context) {
|
|
27
27
|
if (context.data?.twoFactorRedirect) {
|
|
28
28
|
if (options?.onTwoFactorRedirect) {
|
|
29
|
-
await options.onTwoFactorRedirect();
|
|
29
|
+
await options.onTwoFactorRedirect({ twoFactorMethods: context.data.twoFactorMethods });
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
if (options?.twoFactorPage && typeof window !== "undefined") window.location.href = options.twoFactorPage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BackupCodeOptions, backupCode2fa, generateBackupCodes, getBackupCodes, verifyBackupCode } from "./backup-codes/index.mjs";
|
|
1
|
+
import { BackupCodeOptions, backupCode2fa, encodeBackupCodes, generateBackupCodes, getBackupCodes, verifyBackupCode } from "./backup-codes/index.mjs";
|
|
2
2
|
import { OTPOptions, otp2fa } from "./otp/index.mjs";
|
|
3
3
|
import { TOTPOptions, totp2fa } from "./totp/index.mjs";
|
|
4
4
|
import { TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor } from "./types.mjs";
|
|
@@ -618,6 +618,7 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
618
618
|
matcher(context: _better_auth_core0.HookEndpointContext): boolean;
|
|
619
619
|
handler: (inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
|
|
620
620
|
twoFactorRedirect: boolean;
|
|
621
|
+
twoFactorMethods: string[];
|
|
621
622
|
} | undefined>;
|
|
622
623
|
}[];
|
|
623
624
|
};
|
|
@@ -655,6 +656,12 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
655
656
|
};
|
|
656
657
|
index: true;
|
|
657
658
|
};
|
|
659
|
+
verified: {
|
|
660
|
+
type: "boolean";
|
|
661
|
+
required: false;
|
|
662
|
+
defaultValue: true;
|
|
663
|
+
input: false;
|
|
664
|
+
};
|
|
658
665
|
};
|
|
659
666
|
};
|
|
660
667
|
};
|
|
@@ -676,4 +683,4 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
676
683
|
};
|
|
677
684
|
};
|
|
678
685
|
//#endregion
|
|
679
|
-
export { BackupCodeOptions, OTPOptions, TOTPOptions, TWO_FACTOR_ERROR_CODES, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor, backupCode2fa, generateBackupCodes, getBackupCodes, otp2fa, totp2fa, twoFactor, twoFactorClient, verifyBackupCode };
|
|
686
|
+
export { BackupCodeOptions, OTPOptions, TOTPOptions, TWO_FACTOR_ERROR_CODES, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor, backupCode2fa, encodeBackupCodes, generateBackupCodes, getBackupCodes, otp2fa, totp2fa, twoFactor, twoFactorClient, verifyBackupCode };
|
|
@@ -103,6 +103,13 @@ const twoFactor = (options) => {
|
|
|
103
103
|
});
|
|
104
104
|
await ctx.context.internalAdapter.deleteSession(ctx.context.session.session.token);
|
|
105
105
|
}
|
|
106
|
+
const existingTwoFactor = await ctx.context.adapter.findOne({
|
|
107
|
+
model: opts.twoFactorTable,
|
|
108
|
+
where: [{
|
|
109
|
+
field: "userId",
|
|
110
|
+
value: user.id
|
|
111
|
+
}]
|
|
112
|
+
});
|
|
106
113
|
await ctx.context.adapter.deleteMany({
|
|
107
114
|
model: opts.twoFactorTable,
|
|
108
115
|
where: [{
|
|
@@ -115,7 +122,8 @@ const twoFactor = (options) => {
|
|
|
115
122
|
data: {
|
|
116
123
|
secret: encryptedSecret,
|
|
117
124
|
backupCodes: backupCodes.encryptedBackupCodes,
|
|
118
|
-
userId: user.id
|
|
125
|
+
userId: user.id,
|
|
126
|
+
verified: existingTwoFactor != null && existingTwoFactor.verified !== false || !!options?.skipVerificationOnEnable
|
|
119
127
|
}
|
|
120
128
|
});
|
|
121
129
|
const totpURI = createOTP(secret, {
|
|
@@ -181,12 +189,13 @@ const twoFactor = (options) => {
|
|
|
181
189
|
options,
|
|
182
190
|
hooks: { after: [{
|
|
183
191
|
matcher(context) {
|
|
184
|
-
return context.
|
|
192
|
+
return context.context.newSession != null && !context.path?.startsWith("/two-factor/");
|
|
185
193
|
},
|
|
186
194
|
handler: createAuthMiddleware(async (ctx) => {
|
|
187
195
|
const data = ctx.context.newSession;
|
|
188
196
|
if (!data) return;
|
|
189
197
|
if (!data?.user.twoFactorEnabled) return;
|
|
198
|
+
if (ctx.context.session) return;
|
|
190
199
|
const trustDeviceCookieAttrs = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: trustDeviceMaxAge });
|
|
191
200
|
const trustDeviceCookie = await ctx.getSignedCookie(trustDeviceCookieAttrs.name, ctx.context.secret);
|
|
192
201
|
if (trustDeviceCookie) {
|
|
@@ -225,7 +234,30 @@ const twoFactor = (options) => {
|
|
|
225
234
|
expiresAt: new Date(Date.now() + maxAge * 1e3)
|
|
226
235
|
});
|
|
227
236
|
await ctx.setSignedCookie(twoFactorCookie.name, identifier, ctx.context.secret, twoFactorCookie.attributes);
|
|
228
|
-
|
|
237
|
+
const twoFactorMethods = [];
|
|
238
|
+
/**
|
|
239
|
+
* totp requires per-user setup, so we check
|
|
240
|
+
* that the user actually has a secret stored.
|
|
241
|
+
*/
|
|
242
|
+
if (!options?.totpOptions?.disable) {
|
|
243
|
+
const userTotpSecret = await ctx.context.adapter.findOne({
|
|
244
|
+
model: opts.twoFactorTable,
|
|
245
|
+
where: [{
|
|
246
|
+
field: "userId",
|
|
247
|
+
value: data.user.id
|
|
248
|
+
}]
|
|
249
|
+
});
|
|
250
|
+
if (userTotpSecret && userTotpSecret.verified !== false) twoFactorMethods.push("totp");
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* otp is server-level — if sendOTP is configured,
|
|
254
|
+
* any user with 2fa enabled can receive a code.
|
|
255
|
+
*/
|
|
256
|
+
if (options?.otpOptions?.sendOTP) twoFactorMethods.push("otp");
|
|
257
|
+
return ctx.json({
|
|
258
|
+
twoFactorRedirect: true,
|
|
259
|
+
twoFactorMethods
|
|
260
|
+
});
|
|
229
261
|
})
|
|
230
262
|
}] },
|
|
231
263
|
schema: mergeSchema(schema, {
|
|
@@ -175,11 +175,11 @@ const otp2fa = (options) => {
|
|
|
175
175
|
if (!session.session) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
176
176
|
const updatedUser = await ctx.context.internalAdapter.updateUser(session.user.id, { twoFactorEnabled: true });
|
|
177
177
|
const newSession = await ctx.context.internalAdapter.createSession(session.user.id, false, session.session);
|
|
178
|
-
await ctx.context.internalAdapter.deleteSession(session.session.token);
|
|
179
178
|
await setSessionCookie(ctx, {
|
|
180
179
|
session: newSession,
|
|
181
180
|
user: updatedUser
|
|
182
181
|
});
|
|
182
|
+
await ctx.context.internalAdapter.deleteSession(session.session.token);
|
|
183
183
|
return ctx.json({
|
|
184
184
|
token: newSession.token,
|
|
185
185
|
user: parseUserOutput(ctx.context.options, updatedUser)
|
|
@@ -124,6 +124,7 @@ const totp2fa = (options) => {
|
|
|
124
124
|
}
|
|
125
125
|
const { session, valid, invalid } = await verifyTwoFactor(ctx);
|
|
126
126
|
const user = session.user;
|
|
127
|
+
const isSignIn = !session.session;
|
|
127
128
|
const twoFactor = await ctx.context.adapter.findOne({
|
|
128
129
|
model: twoFactorTable,
|
|
129
130
|
where: [{
|
|
@@ -132,6 +133,7 @@ const totp2fa = (options) => {
|
|
|
132
133
|
}]
|
|
133
134
|
});
|
|
134
135
|
if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED);
|
|
136
|
+
if (isSignIn && twoFactor.verified === false) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED);
|
|
135
137
|
if (!await createOTP(await symmetricDecrypt({
|
|
136
138
|
key: ctx.context.secretConfig,
|
|
137
139
|
data: twoFactor.secret
|
|
@@ -139,16 +141,23 @@ const totp2fa = (options) => {
|
|
|
139
141
|
period: opts.period,
|
|
140
142
|
digits: opts.digits
|
|
141
143
|
}).verify(ctx.body.code)) return invalid("INVALID_CODE");
|
|
142
|
-
if (
|
|
143
|
-
if (!
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
if (twoFactor.verified !== true) {
|
|
145
|
+
if (!user.twoFactorEnabled) {
|
|
146
|
+
const activeSession = session.session;
|
|
147
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(user.id, { twoFactorEnabled: true });
|
|
148
|
+
await setSessionCookie(ctx, {
|
|
149
|
+
session: await ctx.context.internalAdapter.createSession(user.id, false, activeSession),
|
|
150
|
+
user: updatedUser
|
|
151
|
+
});
|
|
152
|
+
await ctx.context.internalAdapter.deleteSession(activeSession.token);
|
|
153
|
+
}
|
|
154
|
+
await ctx.context.adapter.update({
|
|
155
|
+
model: twoFactorTable,
|
|
156
|
+
update: { verified: true },
|
|
157
|
+
where: [{
|
|
158
|
+
field: "id",
|
|
159
|
+
value: twoFactor.id
|
|
160
|
+
}]
|
|
152
161
|
});
|
|
153
162
|
}
|
|
154
163
|
return valid(ctx);
|
package/dist/state.d.mts
CHANGED
|
@@ -9,6 +9,11 @@ declare const stateDataSchema: z.ZodObject<{
|
|
|
9
9
|
errorURL: z.ZodOptional<z.ZodString>;
|
|
10
10
|
newUserURL: z.ZodOptional<z.ZodString>;
|
|
11
11
|
expiresAt: z.ZodNumber;
|
|
12
|
+
/**
|
|
13
|
+
* CSRF nonce returned to the OAuth provider. When using cookie state storage,
|
|
14
|
+
* this must match the callback `state` query parameter.
|
|
15
|
+
*/
|
|
16
|
+
oauthState: z.ZodOptional<z.ZodString>;
|
|
12
17
|
link: z.ZodOptional<z.ZodObject<{
|
|
13
18
|
email: z.ZodString;
|
|
14
19
|
userId: z.ZodCoercedString<unknown>;
|
|
@@ -32,6 +37,7 @@ declare function parseGenericState(c: GenericEndpointContext, state: string, set
|
|
|
32
37
|
expiresAt: number;
|
|
33
38
|
errorURL?: string | undefined;
|
|
34
39
|
newUserURL?: string | undefined;
|
|
40
|
+
oauthState?: string | undefined;
|
|
35
41
|
link?: {
|
|
36
42
|
email: string;
|
|
37
43
|
userId: string;
|
package/dist/state.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const stateDataSchema = z.looseObject({
|
|
|
10
10
|
errorURL: z.string().optional(),
|
|
11
11
|
newUserURL: z.string().optional(),
|
|
12
12
|
expiresAt: z.number(),
|
|
13
|
+
oauthState: z.string().optional(),
|
|
13
14
|
link: z.object({
|
|
14
15
|
email: z.string(),
|
|
15
16
|
userId: z.coerce.string()
|
|
@@ -28,9 +29,13 @@ var StateError = class extends BetterAuthError {
|
|
|
28
29
|
async function generateGenericState(c, stateData, settings) {
|
|
29
30
|
const state = generateRandomString(32);
|
|
30
31
|
if (c.context.oauthConfig.storeStateStrategy === "cookie") {
|
|
32
|
+
const payload = {
|
|
33
|
+
...stateData,
|
|
34
|
+
oauthState: state
|
|
35
|
+
};
|
|
31
36
|
const encryptedData = await symmetricEncrypt({
|
|
32
37
|
key: c.context.secretConfig,
|
|
33
|
-
data: JSON.stringify(
|
|
38
|
+
data: JSON.stringify(payload)
|
|
34
39
|
});
|
|
35
40
|
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state", { maxAge: 600 });
|
|
36
41
|
c.setCookie(stateCookie.name, encryptedData, stateCookie.attributes);
|
|
@@ -44,7 +49,10 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
44
49
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
45
50
|
expiresAt.setMinutes(expiresAt.getMinutes() + 10);
|
|
46
51
|
if (!await c.context.internalAdapter.createVerificationValue({
|
|
47
|
-
value: JSON.stringify(
|
|
52
|
+
value: JSON.stringify({
|
|
53
|
+
...stateData,
|
|
54
|
+
oauthState: state
|
|
55
|
+
}),
|
|
48
56
|
identifier: state,
|
|
49
57
|
expiresAt
|
|
50
58
|
})) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" });
|
|
@@ -76,6 +84,10 @@ async function parseGenericState(c, state, settings) {
|
|
|
76
84
|
cause: error
|
|
77
85
|
});
|
|
78
86
|
}
|
|
87
|
+
if (!parsedData.oauthState || parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
|
|
88
|
+
code: "state_security_mismatch",
|
|
89
|
+
details: { state }
|
|
90
|
+
});
|
|
79
91
|
expireCookie(c, stateCookie);
|
|
80
92
|
} else {
|
|
81
93
|
const data = await c.context.internalAdapter.findVerificationValue(state);
|
|
@@ -84,6 +96,10 @@ async function parseGenericState(c, state, settings) {
|
|
|
84
96
|
details: { state }
|
|
85
97
|
});
|
|
86
98
|
parsedData = stateDataSchema.parse(JSON.parse(data.value));
|
|
99
|
+
if (parsedData.oauthState !== void 0 && parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
|
|
100
|
+
code: "state_security_mismatch",
|
|
101
|
+
details: { state }
|
|
102
|
+
});
|
|
87
103
|
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state");
|
|
88
104
|
const stateCookieValue = await c.getSignedCookie(stateCookie.name, c.context.secret);
|
|
89
105
|
if (!(settings?.skipStateCookieCheck ?? c.context.oauthConfig.skipStateCookieCheck) && (!stateCookieValue || stateCookieValue !== state)) throw new StateError("State mismatch: State not persisted correctly", {
|
|
@@ -261,7 +261,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
261
261
|
"application/json": {
|
|
262
262
|
schema: {
|
|
263
263
|
type: "object";
|
|
264
|
-
nullable: boolean;
|
|
265
264
|
properties: {
|
|
266
265
|
session: {
|
|
267
266
|
$ref: string;
|
|
@@ -2265,7 +2264,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
2265
2264
|
"application/json": {
|
|
2266
2265
|
schema: {
|
|
2267
2266
|
type: "object";
|
|
2268
|
-
nullable: boolean;
|
|
2269
2267
|
properties: {
|
|
2270
2268
|
session: {
|
|
2271
2269
|
$ref: string;
|
|
@@ -4272,7 +4270,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
4272
4270
|
"application/json": {
|
|
4273
4271
|
schema: {
|
|
4274
4272
|
type: "object";
|
|
4275
|
-
nullable: boolean;
|
|
4276
4273
|
properties: {
|
|
4277
4274
|
session: {
|
|
4278
4275
|
$ref: string;
|
|
@@ -6276,7 +6273,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
6276
6273
|
"application/json": {
|
|
6277
6274
|
schema: {
|
|
6278
6275
|
type: "object";
|
|
6279
|
-
nullable: boolean;
|
|
6280
6276
|
properties: {
|
|
6281
6277
|
session: {
|
|
6282
6278
|
$ref: string;
|
|
@@ -8354,7 +8350,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
8354
8350
|
"application/json": {
|
|
8355
8351
|
schema: {
|
|
8356
8352
|
type: "object";
|
|
8357
|
-
nullable: boolean;
|
|
8358
8353
|
properties: {
|
|
8359
8354
|
session: {
|
|
8360
8355
|
$ref: string;
|
|
@@ -10358,7 +10353,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
10358
10353
|
"application/json": {
|
|
10359
10354
|
schema: {
|
|
10360
10355
|
type: "object";
|
|
10361
|
-
nullable: boolean;
|
|
10362
10356
|
properties: {
|
|
10363
10357
|
session: {
|
|
10364
10358
|
$ref: string;
|
|
@@ -80,7 +80,13 @@ async function getTestInstance(options, config) {
|
|
|
80
80
|
};
|
|
81
81
|
async function createTestUser() {
|
|
82
82
|
if (config?.disableTestUser) return;
|
|
83
|
-
|
|
83
|
+
const dynamicBaseURL = isDynamicBaseURLConfig(auth.options.baseURL) ? auth.options.baseURL : void 0;
|
|
84
|
+
const host = (dynamicBaseURL?.allowedHosts.find((h) => !h.includes("*") && !h.includes("?")) ?? dynamicBaseURL?.allowedHosts[0])?.replace(/^https?:\/\//, "").split(/[/#]/)[0]?.replace(/\*/g, "test").replace(/\?/g, "x");
|
|
85
|
+
const headers = host ? new Headers({ host }) : void 0;
|
|
86
|
+
await auth.api.signUpEmail({
|
|
87
|
+
body: testUser,
|
|
88
|
+
headers
|
|
89
|
+
});
|
|
84
90
|
}
|
|
85
91
|
if (testWith !== "mongodb") {
|
|
86
92
|
const { runMigrations } = await getMigrations({
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { generateState, parseState } from "../oauth2/state.mjs";
|
|
2
2
|
import { StateData, generateGenericState, parseGenericState } from "../state.mjs";
|
|
3
3
|
import { HIDE_METADATA } from "./hide-metadata.mjs";
|
|
4
|
-
import { getBaseURL, getHost,
|
|
4
|
+
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./url.mjs";
|