check and verify 2fa backup code

This commit is contained in:
Milo Schwartz 2024-10-05 15:45:01 -04:00
parent 4a5e0e1c57
commit 863f94c8db
No known key found for this signature in database
6 changed files with 79 additions and 33 deletions

View file

@ -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<boolean> {
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<boolean> {
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;
}

View file

@ -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(

View file

@ -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(

View file

@ -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) {

View file

@ -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) {

View file

@ -1,15 +0,0 @@
import { decodeHex } from "oslo/encoding";
import { TOTPController } from "oslo/otp";
export async function verifyTotpCode(
code: string,
secret: string,
): Promise<boolean> {
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;
}