diff --git a/packages/better-auth/src/db/schema.ts b/packages/better-auth/src/db/schema.ts index c5390eb20e..dcfcbdd92d 100644 --- a/packages/better-auth/src/db/schema.ts +++ b/packages/better-auth/src/db/schema.ts @@ -61,7 +61,6 @@ export const verificationSchema = z.object({ updatedAt: z.date().default(() => new Date()), expiresAt: z.date(), identifier: z.string(), - nonce: z.string().nullish(), }); export function parseOutputData>( diff --git a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts index 7f633ce4db..60002717b6 100644 --- a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts +++ b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts @@ -3,15 +3,14 @@ import { z } from "zod"; import { createAuthEndpoint } from "../../../api/call"; import { sessionMiddleware } from "../../../api"; import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto"; -import { verifyTwoFactorMiddleware } from "../verify-middleware"; import type { TwoFactorProvider, TwoFactorTable, UserWithTwoFactor, } from "../types"; import { APIError } from "better-call"; -import { setSessionCookie } from "../../../cookies"; import { TWO_FACTOR_ERROR_CODES } from "../error-code"; +import { verifyTwoFactor } from "../verify-two-factor"; export interface BackupCodeOptions { /** @@ -120,7 +119,6 @@ export const backupCode2fa = (options?: BackupCodeOptions) => { }) .optional(), }), - use: [verifyTwoFactorMiddleware], metadata: { openapi: { description: "Verify a backup code for two-factor authentication", @@ -233,7 +231,8 @@ export const backupCode2fa = (options?: BackupCodeOptions) => { }, }, async (ctx) => { - const user = ctx.context.session.user as UserWithTwoFactor; + const { session, valid } = await verifyTwoFactor(ctx); + const user = session.user as UserWithTwoFactor; const twoFactor = await ctx.context.adapter.findOne({ model: twoFactorTable, where: [ @@ -279,14 +278,19 @@ export const backupCode2fa = (options?: BackupCodeOptions) => { }); if (!ctx.body.disableSession) { - await setSessionCookie(ctx, { - session: ctx.context.session.session, - user, - }); + return valid(ctx); } return ctx.json({ - user: user, - session: ctx.context.session, + token: session.session?.token, + user: { + id: session.user?.id, + email: session.user.email, + emailVerified: session.user.emailVerified, + name: session.user.name, + image: session.user.image, + createdAt: session.user.createdAt, + updatedAt: session.user.updatedAt, + }, }); }, ), diff --git a/packages/better-auth/src/plugins/two-factor/error-code.ts b/packages/better-auth/src/plugins/two-factor/error-code.ts index f32dda68fc..9541a6fe45 100644 --- a/packages/better-auth/src/plugins/two-factor/error-code.ts +++ b/packages/better-auth/src/plugins/two-factor/error-code.ts @@ -5,4 +5,8 @@ export const TWO_FACTOR_ERROR_CODES = { TWO_FACTOR_NOT_ENABLED: "Two factor isn't enabled", BACKUP_CODES_NOT_ENABLED: "Backup codes aren't enabled", INVALID_BACKUP_CODE: "Invalid backup code", + INVALID_CODE: "Invalid code", + TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: + "Too many attempts. Please request a new code.", + INVALID_TWO_FACTOR_COOKIE: "Invalid two factor cookie", } as const; diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts index 92d36f18a7..74582757c6 100644 --- a/packages/better-auth/src/plugins/two-factor/index.ts +++ b/packages/better-auth/src/plugins/two-factor/index.ts @@ -296,21 +296,22 @@ export const twoFactor = (options?: TwoFactorOptions) => { */ deleteSessionCookie(ctx, true); await ctx.context.internalAdapter.deleteSession(data.session.token); + const maxAge = options?.otpOptions?.period || 60 * 5; // 5 minutes const twoFactorCookie = ctx.context.createAuthCookie( TWO_FACTOR_COOKIE_NAME, { - maxAge: 60 * 10, // 10 minutes + maxAge, }, ); - /** - * We set the user id and the session - * id as a hash. Later will fetch for - * sessions with the user id compare - * the hash and set that as session. - */ + const identifier = `2fa-${generateRandomString(20)}`; + await ctx.context.internalAdapter.createVerificationValue({ + value: data.user.id, + identifier, + expiresAt: new Date(Date.now() + maxAge * 1000), + }); await ctx.setSignedCookie( twoFactorCookie.name, - data.user.id, + identifier, ctx.context.secret, twoFactorCookie.attributes, ); diff --git a/packages/better-auth/src/plugins/two-factor/otp/index.ts b/packages/better-auth/src/plugins/two-factor/otp/index.ts index 296ceabd55..e466a3ca7a 100644 --- a/packages/better-auth/src/plugins/two-factor/otp/index.ts +++ b/packages/better-auth/src/plugins/two-factor/otp/index.ts @@ -1,7 +1,7 @@ import { APIError } from "better-call"; import { z } from "zod"; import { createAuthEndpoint } from "../../../api/call"; -import { verifyTwoFactorMiddleware } from "../verify-middleware"; +import { verifyTwoFactor } from "../verify-two-factor"; import type { TwoFactorProvider, TwoFactorTable, @@ -10,6 +10,7 @@ import type { import { TWO_FACTOR_ERROR_CODES } from "../error-code"; import { generateRandomString } from "../../../crypto"; import { setSessionCookie } from "../../../cookies"; +import { BASE_ERROR_CODES } from "../../../error/codes"; export interface OTPOptions { /** @@ -48,6 +49,12 @@ export interface OTPOptions { */ request?: Request, ) => Promise | void; + /** + * The number of allowed attempts for the OTP + * + * @default 5 + */ + allowedAttempts?: number; } /** @@ -78,7 +85,6 @@ export const otp2fa = (options?: OTPOptions) => { trustDevice: z.boolean().optional(), }) .optional(), - use: [verifyTwoFactorMiddleware], metadata: { openapi: { summary: "Send two factor OTP", @@ -112,13 +118,13 @@ export const otp2fa = (options?: OTPOptions) => { message: "otp isn't configured", }); } - const user = ctx.context.session.user as UserWithTwoFactor; + const { session, key } = await verifyTwoFactor(ctx); const twoFactor = await ctx.context.adapter.findOne({ model: twoFactorTable, where: [ { field: "userId", - value: user.id, + value: session.user.id, }, ], }); @@ -129,11 +135,14 @@ export const otp2fa = (options?: OTPOptions) => { } const code = generateRandomString(opts.digits, "0-9"); await ctx.context.internalAdapter.createVerificationValue({ - value: code, - identifier: `2fa-otp-${user.id}`, + value: `${code}!0`, + identifier: `2fa-otp-${key}`, expiresAt: new Date(Date.now() + opts.period), }); - await options.sendOTP({ user, otp: code }, ctx.request); + await options.sendOTP( + { user: session.user as UserWithTwoFactor, otp: code }, + ctx.request, + ); return ctx.json({ status: true }); }, ); @@ -153,7 +162,6 @@ export const otp2fa = (options?: OTPOptions) => { */ trustDevice: z.boolean().optional(), }), - use: [verifyTwoFactorMiddleware], metadata: { openapi: { summary: "Verify two factor OTP", @@ -226,13 +234,13 @@ export const otp2fa = (options?: OTPOptions) => { }, }, async (ctx) => { - const user = ctx.context.session.user; + const { session, key, valid, invalid } = await verifyTwoFactor(ctx); const twoFactor = await ctx.context.adapter.findOne({ model: twoFactorTable, where: [ { field: "userId", - value: user.id, + value: session.user.id, }, ], }); @@ -243,39 +251,74 @@ export const otp2fa = (options?: OTPOptions) => { } const toCheckOtp = await ctx.context.internalAdapter.findVerificationValue( - `2fa-otp-${user.id}`, + `2fa-otp-${key}`, ); + const [otp, counter] = toCheckOtp?.value?.split("!") ?? []; if (!toCheckOtp || toCheckOtp.expiresAt < new Date()) { + await ctx.context.internalAdapter.deleteVerificationValue( + `2fa-otp-${key}`, + ); throw new APIError("BAD_REQUEST", { message: TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED, }); } - if (toCheckOtp.value === ctx.body.code) { - if (!user.twoFactorEnabled) { + const allowedAttempts = options?.allowedAttempts || 5; + if (parseInt(counter) >= allowedAttempts) { + await ctx.context.internalAdapter.deleteVerificationValue( + `2fa-otp-${key}`, + ); + throw new APIError("BAD_REQUEST", { + message: TWO_FACTOR_ERROR_CODES.TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE, + }); + } + if (otp === ctx.body.code) { + if (!session.user.twoFactorEnabled) { + if (!session.session) { + throw new APIError("BAD_REQUEST", { + message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION, + }); + } const updatedUser = await ctx.context.internalAdapter.updateUser( - user.id, + session.user.id, { twoFactorEnabled: true, }, ); const newSession = await ctx.context.internalAdapter.createSession( - user.id, + session.user.id, ctx.headers, false, - ctx.context.session.session, + session.session, ); await ctx.context.internalAdapter.deleteSession( - ctx.context.session.session.token, + session.session.token, ); - await setSessionCookie(ctx, { session: newSession, user: updatedUser, }); + return ctx.json({ + token: newSession.token, + user: { + id: updatedUser.id, + email: updatedUser.email, + emailVerified: updatedUser.emailVerified, + name: updatedUser.name, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); } - return ctx.context.valid(ctx); + return valid(ctx); } else { - return ctx.context.invalid(); + await ctx.context.internalAdapter.updateVerificationValue( + toCheckOtp.id, + { + value: `${otp}!${parseInt(counter) + 1}`, + }, + ); + return invalid("INVALID_CODE"); } }, ); diff --git a/packages/better-auth/src/plugins/two-factor/totp/index.ts b/packages/better-auth/src/plugins/two-factor/totp/index.ts index 3945cdd1cf..9343a0aacb 100644 --- a/packages/better-auth/src/plugins/two-factor/totp/index.ts +++ b/packages/better-auth/src/plugins/two-factor/totp/index.ts @@ -4,7 +4,7 @@ import { createAuthEndpoint } from "../../../api/call"; import { sessionMiddleware } from "../../../api"; import { symmetricDecrypt } from "../../../crypto"; import type { BackupCodeOptions } from "../backup-codes"; -import { verifyTwoFactorMiddleware } from "../verify-middleware"; +import { verifyTwoFactor } from "../verify-two-factor"; import type { TwoFactorProvider, TwoFactorTable, @@ -13,6 +13,7 @@ import type { import { setSessionCookie } from "../../../cookies"; import { TWO_FACTOR_ERROR_CODES } from "../error-code"; import { createOTP } from "@better-auth/utils/otp"; +import { BASE_ERROR_CODES } from "../../../error/codes"; export type TOTPOptions = { /** @@ -53,7 +54,11 @@ export const totp2fa = (options?: TOTPOptions) => { "/totp/generate", { method: "POST", - use: [sessionMiddleware], + body: z.object({ + secret: z.string({ + description: "The secret to generate the TOTP code", + }), + }), metadata: { openapi: { summary: "Generate TOTP code", @@ -76,6 +81,7 @@ export const totp2fa = (options?: TOTPOptions) => { }, }, }, + SERVER_ONLY: true, }, }, async (ctx) => { @@ -87,22 +93,7 @@ export const totp2fa = (options?: TOTPOptions) => { message: "totp isn't configured", }); } - const user = ctx.context.session.user as UserWithTwoFactor; - const twoFactor = await ctx.context.adapter.findOne({ - model: twoFactorTable, - where: [ - { - field: "userId", - value: user.id, - }, - ], - }); - if (!twoFactor) { - throw new APIError("BAD_REQUEST", { - message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED, - }); - } - const code = await createOTP(twoFactor.secret, { + const code = await createOTP(ctx.body.secret, { period: opts.period, digits: opts.digits, }).totp(); @@ -203,7 +194,6 @@ export const totp2fa = (options?: TOTPOptions) => { }) .optional(), }), - use: [verifyTwoFactorMiddleware], metadata: { openapi: { summary: "Verify two factor TOTP", @@ -237,7 +227,8 @@ export const totp2fa = (options?: TOTPOptions) => { message: "totp isn't configured", }); } - const user = ctx.context.session.user as UserWithTwoFactor; + const { session, valid, invalid } = await verifyTwoFactor(ctx); + const user = session.user as UserWithTwoFactor; const twoFactor = await ctx.context.adapter.findOne({ model: twoFactorTable, where: [ @@ -262,10 +253,15 @@ export const totp2fa = (options?: TOTPOptions) => { digits: opts.digits, }).verify(ctx.body.code); if (!status) { - return ctx.context.invalid(); + return invalid("INVALID_CODE"); } if (!user.twoFactorEnabled) { + if (!session.session) { + throw new APIError("BAD_REQUEST", { + message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION, + }); + } const updatedUser = await ctx.context.internalAdapter.updateUser( user.id, { @@ -274,25 +270,18 @@ export const totp2fa = (options?: TOTPOptions) => { ctx, ); const newSession = await ctx.context.internalAdapter - .createSession( - user.id, - ctx.headers, - false, - ctx.context.session.session, - ) + .createSession(user.id, ctx.headers, false, session.session) .catch((e) => { throw e; }); - await ctx.context.internalAdapter.deleteSession( - ctx.context.session.session.token, - ); + await ctx.context.internalAdapter.deleteSession(session.session.token); await setSessionCookie(ctx, { session: newSession, user: updatedUser, }); } - return ctx.context.valid(ctx); + return valid(ctx); }, ); return { diff --git a/packages/better-auth/src/plugins/two-factor/two-factor.test.ts b/packages/better-auth/src/plugins/two-factor/two-factor.test.ts index 99ce053c4a..c3cf1e8d3f 100644 --- a/packages/better-auth/src/plugins/two-factor/two-factor.test.ts +++ b/packages/better-auth/src/plugins/two-factor/two-factor.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; -import { twoFactor, twoFactorClient } from "."; +import { TWO_FACTOR_ERROR_CODES, twoFactor, twoFactorClient } from "."; import { createAuthClient } from "../../client"; import { parseSetCookieHeader } from "../../cookies"; import type { TwoFactorTable, UserWithTwoFactor } from "./types"; @@ -52,7 +52,6 @@ describe("two factor", async () => { headers, }, }); - expect(res.data?.backupCodes.length).toEqual(10); expect(res.data?.totpURI).toBeDefined(); const dbUser = await db.findOne({ @@ -193,6 +192,18 @@ describe("two factor", async () => { expect(verifyRes.data?.token).toBeDefined(); }); + it("should fail if two factor cookie is missing", async () => { + const res = await client.twoFactor.verifyTotp({ + code: "123456", + fetchOptions: { + headers, + }, + }); + expect(res.error?.message).toBe( + TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, + ); + }); + let backupCodes: string[] = []; it("should generate backup codes", async () => { await client.twoFactor.enable({ @@ -338,6 +349,53 @@ describe("two factor", async () => { expect(signInRes.data?.user).toBeDefined(); }); + it("should limit OTP verification attempts", async () => { + const headers = new Headers(); + // Sign in to trigger 2FA + await client.signIn.email({ + email: testUser.email, + password: testUser.password, + fetchOptions: { + onSuccess(context) { + const parsed = parseSetCookieHeader( + context.response.headers.get("Set-Cookie") || "", + ); + headers.append( + "cookie", + `better-auth.two_factor=${ + parsed.get("better-auth.two_factor")?.value + }`, + ); + }, + }, + }); + await client.twoFactor.sendOtp({ + fetchOptions: { + headers, + }, + }); + for (let i = 0; i < 5; i++) { + const res = await client.twoFactor.verifyOtp({ + code: "000000", // Invalid code + fetchOptions: { + headers, + }, + }); + expect(res.error?.message).toBe("Invalid code"); + } + + // Next attempt should be blocked + const res = await client.twoFactor.verifyOtp({ + code: OTP, // Even with correct code + fetchOptions: { + headers, + }, + }); + expect(res.error?.message).toBe( + "Too many attempts. Please request a new code.", + ); + }); + it("should disable two factor", async () => { const res = await client.twoFactor.disable({ password: testUser.password, diff --git a/packages/better-auth/src/plugins/two-factor/verify-middleware.ts b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts deleted file mode 100644 index 632f825f05..0000000000 --- a/packages/better-auth/src/plugins/two-factor/verify-middleware.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { APIError } from "better-call"; -import { createAuthMiddleware } from "../../api/call"; -import { TRUST_DEVICE_COOKIE_NAME, TWO_FACTOR_COOKIE_NAME } from "./constant"; -import { setSessionCookie } from "../../cookies"; -import { z } from "zod"; -import { getSessionFromCtx } from "../../api"; -import type { UserWithTwoFactor } from "./types"; -import { createHMAC } from "@better-auth/utils/hmac"; -import type { GenericEndpointContext } from "../../types"; - -export const verifyTwoFactorMiddleware = createAuthMiddleware( - { - body: z.object({ - /** - * 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().optional(), - }), - }, - async (ctx) => { - const session = await getSessionFromCtx(ctx); - if (!session) { - const cookieName = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME); - const userId = await ctx.getSignedCookie( - cookieName.name, - ctx.context.secret, - ); - if (!userId) { - throw new APIError("UNAUTHORIZED", { - message: "invalid two factor cookie", - }); - } - const user = (await ctx.context.internalAdapter.findUserById( - userId, - )) as UserWithTwoFactor; - if (!user) { - throw new APIError("UNAUTHORIZED", { - message: "invalid two factor cookie", - }); - } - const dontRememberMe = await ctx.getSignedCookie( - ctx.context.authCookies.dontRememberToken.name, - ctx.context.secret, - ); - const session = await ctx.context.internalAdapter.createSession( - userId, - ctx.headers, - !!dontRememberMe, - ); - if (!session) { - throw new APIError("INTERNAL_SERVER_ERROR", { - message: "failed to create session", - }); - } - return { - valid: async (ctx: GenericEndpointContext) => { - await setSessionCookie(ctx, { - session, - user, - }); - if (ctx.body.trustDevice) { - const trustDeviceCookie = ctx.context.createAuthCookie( - TRUST_DEVICE_COOKIE_NAME, - { - maxAge: 30 * 24 * 60 * 60, // 30 days, it'll be refreshed on sign in requests - }, - ); - /** - * create a token that will be used to - * verify the device - */ - const token = await createHMAC("SHA-256", "base64urlnopad").sign( - ctx.context.secret, - `${user.id}!${session.token}`, - ); - await ctx.setSignedCookie( - trustDeviceCookie.name, - `${token}!${session.token}`, - ctx.context.secret, - trustDeviceCookie.attributes, - ); - // delete the dont remember me cookie - ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", { - maxAge: 0, - }); - // delete the two factor cookie - ctx.setCookie(cookieName.name, "", { - maxAge: 0, - }); - } - return ctx.json({ - token: session.token, - user: { - id: user.id, - email: user.email, - emailVerified: user.emailVerified, - name: user.name, - image: user.image, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - }); - }, - invalid: async () => { - throw new APIError("UNAUTHORIZED", { - message: "invalid two factor authentication", - }); - }, - session: { - session, - user, - }, - }; - } - return { - valid: async (ctx: GenericEndpointContext) => { - return ctx.json({ - token: session.session.token, - user: { - id: session.user.id, - email: session.user.email, - emailVerified: session.user.emailVerified, - name: session.user.name, - image: session.user.image, - createdAt: session.user.createdAt, - updatedAt: session.user.updatedAt, - }, - }); - }, - invalid: async () => { - throw new APIError("UNAUTHORIZED", { - message: "invalid two factor authentication", - }); - }, - session, - }; - }, -); diff --git a/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts b/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts new file mode 100644 index 0000000000..81f364b0c2 --- /dev/null +++ b/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts @@ -0,0 +1,136 @@ +import { APIError } from "better-call"; +import { TRUST_DEVICE_COOKIE_NAME, TWO_FACTOR_COOKIE_NAME } from "./constant"; +import { setSessionCookie } from "../../cookies"; +import { getSessionFromCtx } from "../../api"; +import type { UserWithTwoFactor } from "./types"; +import { createHMAC } from "@better-auth/utils/hmac"; +import type { GenericEndpointContext } from "../../types"; +import { TWO_FACTOR_ERROR_CODES } from "./error-code"; + +export async function verifyTwoFactor(ctx: GenericEndpointContext) { + const session = await getSessionFromCtx(ctx); + if (!session) { + const cookieName = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME); + const twoFactorCookie = await ctx.getSignedCookie( + cookieName.name, + ctx.context.secret, + ); + if (!twoFactorCookie) { + throw new APIError("UNAUTHORIZED", { + message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, + }); + } + const verificationToken = + await ctx.context.internalAdapter.findVerificationValue(twoFactorCookie); + if (!verificationToken) { + throw new APIError("UNAUTHORIZED", { + message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, + }); + } + const user = (await ctx.context.internalAdapter.findUserById( + verificationToken.value, + )) as UserWithTwoFactor; + if (!user) { + throw new APIError("UNAUTHORIZED", { + message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, + }); + } + const dontRememberMe = await ctx.getSignedCookie( + ctx.context.authCookies.dontRememberToken.name, + ctx.context.secret, + ); + return { + valid: async (ctx: GenericEndpointContext) => { + const session = await ctx.context.internalAdapter.createSession( + verificationToken.value, + ctx.headers, + !!dontRememberMe, + ); + if (!session) { + throw new APIError("INTERNAL_SERVER_ERROR", { + message: "failed to create session", + }); + } + await setSessionCookie(ctx, { + session, + user, + }); + if (ctx.body.trustDevice) { + const trustDeviceCookie = ctx.context.createAuthCookie( + TRUST_DEVICE_COOKIE_NAME, + { + maxAge: 30 * 24 * 60 * 60, // 30 days, it'll be refreshed on sign in requests + }, + ); + /** + * create a token that will be used to + * verify the device + */ + const token = await createHMAC("SHA-256", "base64urlnopad").sign( + ctx.context.secret, + `${user.id}!${session.token}`, + ); + await ctx.setSignedCookie( + trustDeviceCookie.name, + `${token}!${session.token}`, + ctx.context.secret, + trustDeviceCookie.attributes, + ); + // delete the dont remember me cookie + ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", { + maxAge: 0, + }); + // delete the two factor cookie + ctx.setCookie(cookieName.name, "", { + maxAge: 0, + }); + } + return ctx.json({ + token: session.token, + user: { + id: user.id, + email: user.email, + emailVerified: user.emailVerified, + name: user.name, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + }, + invalid: async (errorKey: keyof typeof TWO_FACTOR_ERROR_CODES) => { + throw new APIError("UNAUTHORIZED", { + message: TWO_FACTOR_ERROR_CODES[errorKey], + }); + }, + session: { + session: null, + user, + }, + key: twoFactorCookie, + }; + } + return { + valid: async (ctx: GenericEndpointContext) => { + return ctx.json({ + token: session.session.token, + user: { + id: session.user.id, + email: session.user.email, + emailVerified: session.user.emailVerified, + name: session.user.name, + image: session.user.image, + createdAt: session.user.createdAt, + updatedAt: session.user.updatedAt, + }, + }); + }, + invalid: async () => { + throw new APIError("UNAUTHORIZED", { + message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, + }); + }, + session, + key: `${session.user.id}!${session.session.id}`, + }; +}