diff --git a/server/routers/auth/2fa.ts b/server/routers/auth/2fa.ts new file mode 100644 index 00000000..57430b35 --- /dev/null +++ b/server/routers/auth/2fa.ts @@ -0,0 +1,63 @@ +import { verify } from "@node-rs/argon2"; +import db from "@server/db"; +import { twoFactorBackupCodes } from "@server/db/schema"; +import { eq } from "drizzle-orm"; +import { decodeHex } from "oslo/encoding"; +import { TOTPController } from "oslo/otp"; + +export async function verifyTotpCode( + code: string, + secret: string, + userId: string, +): Promise { + if (code.length !== 6) { + const validBackupCode = await verifyBackUpCode(code, userId); + return validBackupCode; + } else { + const validOTP = await new TOTPController().verify( + code, + decodeHex(secret), + ); + + if (!validOTP) { + await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attack + } + + return validOTP; + } +} + +export async function verifyBackUpCode( + code: string, + userId: string, +): Promise { + const allHashed = await db + .select() + .from(twoFactorBackupCodes) + .where(eq(twoFactorBackupCodes.userId, userId)); + + if (!allHashed || !allHashed.length) { + return false; + } + + let validId; + for (const hashedCode of allHashed) { + const validCode = await verify(hashedCode.codeHash, code, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + if (validCode) { + validId = hashedCode.id; + } + } + + if (validId) { + await db + .delete(twoFactorBackupCodes) + .where(eq(twoFactorBackupCodes.id, validId)); + } + + return validId ? true : false; +} diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts index 4e201e02..af2d8317 100644 --- a/server/routers/auth/changePassword.ts +++ b/server/routers/auth/changePassword.ts @@ -9,9 +9,8 @@ import { User, users } from "@server/db/schema"; import { eq } from "drizzle-orm"; import { response } from "@server/utils"; import { hashPassword, verifyPassword } from "./password"; -import { verifyTotpCode } from "./verifyTotpCode"; +import { verifyTotpCode } from "./2fa"; import { passwordSchema } from "./passwordSchema"; -import logger from "@server/logger"; export const changePasswordBody = z.object({ oldPassword: z.string(), @@ -72,7 +71,11 @@ export async function changePassword( status: HttpCode.ACCEPTED, }); } - const validOTP = await verifyTotpCode(code!, user.twoFactorSecret!); + const validOTP = await verifyTotpCode( + code!, + user.twoFactorSecret!, + user.id, + ); if (!validOTP) { return next( diff --git a/server/routers/auth/disable2fa.ts b/server/routers/auth/disable2fa.ts index 6c4221a2..6bd4e2db 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -9,7 +9,7 @@ import { twoFactorBackupCodes, User, users } from "@server/db/schema"; import { eq } from "drizzle-orm"; import { response } from "@server/utils"; import { verifyPassword } from "./password"; -import { verifyTotpCode } from "./verifyTotpCode"; +import { verifyTotpCode } from "./2fa"; export const disable2faBody = z.object({ password: z.string(), @@ -66,7 +66,11 @@ export async function disable2fa( } } - const validOTP = await verifyTotpCode(code, user.twoFactorSecret!); + const validOTP = await verifyTotpCode( + code, + user.twoFactorSecret!, + user.id, + ); if (!validOTP) { return next( diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 92430438..3fc68dc7 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -2,19 +2,14 @@ import { verify } from "@node-rs/argon2"; import lucia, { verifySession } from "@server/auth"; import db from "@server/db"; import { users } from "@server/db/schema"; -import { sendEmail } from "@server/emails"; -import { VerifyEmail } from "@server/emails/templates/verifyEmailCode"; -import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/utils/response"; import { eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import { decodeHex } from "oslo/encoding"; -import { TOTPController } from "oslo/otp"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { verifyTotpCode } from "./verifyTotpCode"; +import { verifyTotpCode } from "./2fa"; export const loginBodySchema = z.object({ email: z.string().email(), @@ -108,6 +103,7 @@ export async function login( const validOTP = await verifyTotpCode( code, existingUser.twoFactorSecret!, + existingUser.id, ); if (!validOTP) { diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index cd9a52bd..f9f0d15f 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -2,8 +2,6 @@ import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { decodeHex } from "oslo/encoding"; -import { TOTPController } from "oslo/otp"; import HttpCode from "@server/types/HttpCode"; import { response } from "@server/utils"; import { db } from "@server/db"; @@ -11,6 +9,7 @@ import { twoFactorBackupCodes, User, users } from "@server/db/schema"; import { eq } from "drizzle-orm"; import { alphabet, generateRandomString } from "oslo/crypto"; import { hashPassword } from "./password"; +import { verifyTotpCode } from "./2fa"; export const verifyTotpBody = z.object({ code: z.string(), @@ -62,11 +61,7 @@ export async function verifyTotp( } try { - const totpController = new TOTPController(); - const valid = await totpController.verify( - code, - decodeHex(user.twoFactorSecret), - ); + const valid = await verifyTotpCode(code, user.twoFactorSecret, user.id); const backupCodes = await generateBackupCodes(); for (const code of backupCodes) { diff --git a/server/routers/auth/verifyTotpCode.ts b/server/routers/auth/verifyTotpCode.ts deleted file mode 100644 index 354ce85d..00000000 --- a/server/routers/auth/verifyTotpCode.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { decodeHex } from "oslo/encoding"; -import { TOTPController } from "oslo/otp"; - -export async function verifyTotpCode( - code: string, - secret: string, -): Promise { - const validOTP = await new TOTPController().verify(code, decodeHex(secret)); - - if (!validOTP) { - await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attack - } - - return validOTP; -}