better-auth
Version:
The most comprehensive authentication framework for TypeScript.
208 lines (206 loc) • 8.24 kB
JavaScript
import { mergeSchema } from "../../db/schema.mjs";
import { generateRandomString } from "../../crypto/random.mjs";
import { symmetricEncrypt } from "../../crypto/index.mjs";
import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
import { sessionMiddleware } from "../../api/routes/session.mjs";
import "../../api/index.mjs";
import { validatePassword } from "../../utils/password.mjs";
import { twoFactorClient } from "./client.mjs";
import { TWO_FACTOR_ERROR_CODES } from "./error-code.mjs";
import { TRUST_DEVICE_COOKIE_MAX_AGE, TRUST_DEVICE_COOKIE_NAME, TWO_FACTOR_COOKIE_NAME } from "./constant.mjs";
import { backupCode2fa, generateBackupCodes } from "./backup-codes/index.mjs";
import { otp2fa } from "./otp/index.mjs";
import { schema } from "./schema.mjs";
import { totp2fa } from "./totp/index.mjs";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { APIError } from "better-call";
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
import { createHMAC } from "@better-auth/utils/hmac";
import { createOTP } from "@better-auth/utils/otp";
//#region src/plugins/two-factor/index.ts
const enableTwoFactorBodySchema = z.object({
password: z.string().meta({ description: "User password" }),
issuer: z.string().meta({ description: "Custom issuer for the TOTP URI" }).optional()
});
const disableTwoFactorBodySchema = z.object({ password: z.string().meta({ description: "User password" }) });
const twoFactor = (options) => {
const opts = { twoFactorTable: "twoFactor" };
const backupCodeOptions = {
storeBackupCodes: "encrypted",
...options?.backupCodeOptions
};
const totp = totp2fa(options?.totpOptions);
const backupCode = backupCode2fa(backupCodeOptions);
const otp = otp2fa(options?.otpOptions);
return {
id: "two-factor",
endpoints: {
...totp.endpoints,
...otp.endpoints,
...backupCode.endpoints,
enableTwoFactor: createAuthEndpoint("/two-factor/enable", {
method: "POST",
body: enableTwoFactorBodySchema,
use: [sessionMiddleware],
metadata: { openapi: {
summary: "Enable two factor authentication",
description: "Use this endpoint to enable two factor authentication. This will generate a TOTP URI and backup codes. Once the user verifies the TOTP URI, the two factor authentication will be enabled.",
responses: { 200: {
description: "Successful response",
content: { "application/json": { schema: {
type: "object",
properties: {
totpURI: {
type: "string",
description: "TOTP URI"
},
backupCodes: {
type: "array",
items: { type: "string" },
description: "Backup codes"
}
}
} } }
} }
} }
}, async (ctx) => {
const user = ctx.context.session.user;
const { password, issuer } = ctx.body;
if (!await validatePassword(ctx, {
password,
userId: user.id
})) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD });
const secret = generateRandomString(32);
const encryptedSecret = await symmetricEncrypt({
key: ctx.context.secret,
data: secret
});
const backupCodes = await generateBackupCodes(ctx.context.secret, backupCodeOptions);
if (options?.skipVerificationOnEnable) {
const updatedUser = await ctx.context.internalAdapter.updateUser(user.id, { twoFactorEnabled: true });
/**
* Update the session cookie with the new user data
*/
await setSessionCookie(ctx, {
session: await ctx.context.internalAdapter.createSession(updatedUser.id, false, ctx.context.session.session),
user: updatedUser
});
await ctx.context.internalAdapter.deleteSession(ctx.context.session.session.token);
}
await ctx.context.adapter.deleteMany({
model: opts.twoFactorTable,
where: [{
field: "userId",
value: user.id
}]
});
await ctx.context.adapter.create({
model: opts.twoFactorTable,
data: {
secret: encryptedSecret,
backupCodes: backupCodes.encryptedBackupCodes,
userId: user.id
}
});
const totpURI = createOTP(secret, {
digits: options?.totpOptions?.digits || 6,
period: options?.totpOptions?.period
}).url(issuer || options?.issuer || ctx.context.appName, user.email);
return ctx.json({
totpURI,
backupCodes: backupCodes.backupCodes
});
}),
disableTwoFactor: createAuthEndpoint("/two-factor/disable", {
method: "POST",
body: disableTwoFactorBodySchema,
use: [sessionMiddleware],
metadata: { openapi: {
summary: "Disable two factor authentication",
description: "Use this endpoint to disable two factor authentication.",
responses: { 200: {
description: "Successful response",
content: { "application/json": { schema: {
type: "object",
properties: { status: { type: "boolean" } }
} } }
} }
} }
}, async (ctx) => {
const user = ctx.context.session.user;
const { password } = ctx.body;
if (!await validatePassword(ctx, {
password,
userId: user.id
})) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_PASSWORD });
const updatedUser = await ctx.context.internalAdapter.updateUser(user.id, { twoFactorEnabled: false });
await ctx.context.adapter.delete({
model: opts.twoFactorTable,
where: [{
field: "userId",
value: updatedUser.id
}]
});
/**
* Update the session cookie with the new user data
*/
await setSessionCookie(ctx, {
session: await ctx.context.internalAdapter.createSession(updatedUser.id, false, ctx.context.session.session),
user: updatedUser
});
await ctx.context.internalAdapter.deleteSession(ctx.context.session.session.token);
return ctx.json({ status: true });
})
},
options,
hooks: { after: [{
matcher(context) {
return context.path === "/sign-in/email" || context.path === "/sign-in/username" || context.path === "/sign-in/phone-number";
},
handler: createAuthMiddleware(async (ctx) => {
const data = ctx.context.newSession;
if (!data) return;
if (!data?.user.twoFactorEnabled) return;
const trustDeviceCookieAttrs = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: TRUST_DEVICE_COOKIE_MAX_AGE });
const trustDeviceCookie = await ctx.getSignedCookie(trustDeviceCookieAttrs.name, ctx.context.secret);
if (trustDeviceCookie) {
const [token, sessionToken] = trustDeviceCookie.split("!");
if (token === await createHMAC("SHA-256", "base64urlnopad").sign(ctx.context.secret, `${data.user.id}!${sessionToken}`)) {
const newTrustDeviceCookie = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: TRUST_DEVICE_COOKIE_MAX_AGE });
const newToken = await createHMAC("SHA-256", "base64urlnopad").sign(ctx.context.secret, `${data.user.id}!${data.session.token}`);
await ctx.setSignedCookie(newTrustDeviceCookie.name, `${newToken}!${data.session.token}`, ctx.context.secret, trustDeviceCookieAttrs.attributes);
return;
}
}
/**
* remove the session cookie. It's set by the sign in credential
*/
deleteSessionCookie(ctx, true);
await ctx.context.internalAdapter.deleteSession(data.session.token);
const maxAge = (options?.otpOptions?.period ?? 3) * 60;
const twoFactorCookie = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME, { maxAge });
const identifier = `2fa-${generateRandomString(20)}`;
await ctx.context.internalAdapter.createVerificationValue({
value: data.user.id,
identifier,
expiresAt: new Date(Date.now() + maxAge * 1e3)
});
await ctx.setSignedCookie(twoFactorCookie.name, identifier, ctx.context.secret, twoFactorCookie.attributes);
return ctx.json({ twoFactorRedirect: true });
})
}] },
schema: mergeSchema(schema, options?.schema),
rateLimit: [{
pathMatcher(path) {
return path.startsWith("/two-factor/");
},
window: 10,
max: 3
}],
$ERROR_CODES: TWO_FACTOR_ERROR_CODES
};
};
//#endregion
export { TWO_FACTOR_ERROR_CODES, twoFactor, twoFactorClient };
//# sourceMappingURL=index.mjs.map