UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

231 lines (230 loc) • 7.88 kB
import { symmetricDecrypt } from "../../../crypto/index.mjs"; import { setSessionCookie } from "../../../cookies/index.mjs"; import { sessionMiddleware } from "../../../api/routes/session.mjs"; import { shouldRequirePassword } from "../../../utils/password.mjs"; import { PACKAGE_VERSION } from "../../../version.mjs"; import { TWO_FACTOR_ERROR_CODES } from "../error-code.mjs"; import { assertTwoFactorNotLocked, recordTwoFactorFailure, resetTwoFactorFailures, verifyTwoFactor } from "../verify-two-factor.mjs"; import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error"; import { createAuthEndpoint } from "@better-auth/core/api"; import * as z from "zod"; import { createOTP } from "@better-auth/utils/otp"; //#region src/plugins/two-factor/totp/index.ts const generateTOTPBodySchema = z.object({ secret: z.string().meta({ description: "The secret to generate the TOTP code" }) }); const verifyTOTPBodySchema = z.object({ code: z.string().meta({ description: "The otp code to verify. Eg: \"012345\"" }), /** * if true, the device will be trusted * for 30 days. It'll be refreshed on * every sign in request within this time. */ trustDevice: z.boolean().meta({ description: "If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true" }).optional() }); const totp2fa = (options) => { const opts = { ...options, digits: options?.digits || 6, period: options?.period || 30 }; const passwordSchema = z.string().meta({ description: "User password" }); const getTOTPURIBodySchema = options?.allowPasswordless ? z.object({ password: passwordSchema.optional() }) : z.object({ password: passwordSchema }); const twoFactorTable = "twoFactor"; return { id: "totp", version: PACKAGE_VERSION, endpoints: { /** * ### Endpoint * * POST `/totp/generate` * * ### API Methods * * **server:** * `auth.api.generateTOTP` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#totp) */ generateTOTP: createAuthEndpoint.serverOnly({ method: "POST", body: generateTOTPBodySchema, metadata: { openapi: { summary: "Generate TOTP code", description: "Use this endpoint to generate a TOTP code", responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { code: { type: "string" } } } } } } } } } }, async (ctx) => { if (options?.disable) { ctx.context.logger.error("totp isn't configured. please pass totp option on two factor plugin to enable totp"); throw APIError.from("BAD_REQUEST", { message: "totp isn't configured", code: "TOTP_NOT_CONFIGURED" }); } return { code: await createOTP(ctx.body.secret, { period: opts.period, digits: opts.digits }).totp() }; }), /** * ### Endpoint * * POST `/two-factor/get-totp-uri` * * ### API Methods * * **server:** * `auth.api.getTOTPURI` * * **client:** * `authClient.twoFactor.getTotpUri` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#getting-totp-uri) */ getTOTPURI: createAuthEndpoint("/two-factor/get-totp-uri", { method: "POST", use: [sessionMiddleware], body: getTOTPURIBodySchema, metadata: { openapi: { summary: "Get TOTP URI", description: "Use this endpoint to get the TOTP URI", responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { totpURI: { type: "string" } } } } } } } } } }, async (ctx) => { if (options?.disable) { ctx.context.logger.error("totp isn't configured. please pass totp option on two factor plugin to enable totp"); throw APIError.from("BAD_REQUEST", { message: "totp isn't configured", code: "TOTP_NOT_CONFIGURED" }); } const user = ctx.context.session.user; const twoFactor = await ctx.context.adapter.findOne({ model: twoFactorTable, where: [{ field: "userId", value: user.id }] }); if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED); const secret = await symmetricDecrypt({ key: ctx.context.secretConfig, data: twoFactor.secret }); if (await shouldRequirePassword(ctx, user.id, options?.allowPasswordless)) { if (!ctx.body.password) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD); await ctx.context.password.checkPassword(user.id, ctx); } return { totpURI: createOTP(secret, { digits: opts.digits, period: opts.period }).url(options?.issuer || ctx.context.appName, user.email) }; }), /** * ### Endpoint * * POST `/two-factor/verify-totp` * * ### API Methods * * **server:** * `auth.api.verifyTOTP` * * **client:** * `authClient.twoFactor.verifyTotp` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#verifying-totp) */ verifyTOTP: createAuthEndpoint("/two-factor/verify-totp", { method: "POST", body: verifyTOTPBodySchema, metadata: { openapi: { summary: "Verify two factor TOTP", description: "Verify two factor TOTP", responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" } } } } } } } } } }, async (ctx) => { if (options?.disable) { ctx.context.logger.error("totp isn't configured. please pass totp option on two factor plugin to enable totp"); throw APIError.from("BAD_REQUEST", { message: "totp isn't configured", code: "TOTP_NOT_CONFIGURED" }); } const { session, valid, invalid, beginAttempt } = await verifyTwoFactor(ctx); const user = session.user; const isSignIn = !session.session; const twoFactor = await ctx.context.adapter.findOne({ model: twoFactorTable, where: [{ field: "userId", value: user.id }] }); if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED); if (isSignIn && twoFactor.verified === false) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED); if (isSignIn) await assertTwoFactorNotLocked(ctx, twoFactorTable, twoFactor); const attempt = isSignIn ? await beginAttempt(5) : null; let status; try { status = await createOTP(await symmetricDecrypt({ key: ctx.context.secretConfig, data: twoFactor.secret }), { period: opts.period, digits: opts.digits }).verify(ctx.body.code); } catch (error) { await attempt?.restore(); throw error; } if (!status) { await attempt?.recordFailure(); if (isSignIn) await recordTwoFactorFailure(ctx, twoFactorTable, twoFactor); return invalid("INVALID_CODE"); } if (isSignIn) await resetTwoFactorFailures(ctx, twoFactorTable, twoFactor); if (twoFactor.verified !== true) { if (!user.twoFactorEnabled) { const activeSession = session.session; const updatedUser = await ctx.context.internalAdapter.updateUser(user.id, { twoFactorEnabled: true }); await setSessionCookie(ctx, { session: await ctx.context.internalAdapter.createSession(user.id, false, activeSession), user: updatedUser }); await ctx.context.internalAdapter.deleteSession(activeSession.token); } await ctx.context.adapter.update({ model: twoFactorTable, update: { verified: true }, where: [{ field: "id", value: twoFactor.id }] }); } return valid(ctx); }) } }; }; //#endregion export { totp2fa };