mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-28 05:44:01 +02:00
check and verify 2fa backup code
This commit is contained in:
parent
4a5e0e1c57
commit
863f94c8db
6 changed files with 79 additions and 33 deletions
63
server/routers/auth/2fa.ts
Normal file
63
server/routers/auth/2fa.ts
Normal 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;
|
||||||
|
}
|
|
@ -9,9 +9,8 @@ import { User, users } from "@server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { response } from "@server/utils";
|
import { response } from "@server/utils";
|
||||||
import { hashPassword, verifyPassword } from "./password";
|
import { hashPassword, verifyPassword } from "./password";
|
||||||
import { verifyTotpCode } from "./verifyTotpCode";
|
import { verifyTotpCode } from "./2fa";
|
||||||
import { passwordSchema } from "./passwordSchema";
|
import { passwordSchema } from "./passwordSchema";
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export const changePasswordBody = z.object({
|
export const changePasswordBody = z.object({
|
||||||
oldPassword: z.string(),
|
oldPassword: z.string(),
|
||||||
|
@ -72,7 +71,11 @@ export async function changePassword(
|
||||||
status: HttpCode.ACCEPTED,
|
status: HttpCode.ACCEPTED,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const validOTP = await verifyTotpCode(code!, user.twoFactorSecret!);
|
const validOTP = await verifyTotpCode(
|
||||||
|
code!,
|
||||||
|
user.twoFactorSecret!,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (!validOTP) {
|
if (!validOTP) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { twoFactorBackupCodes, User, users } from "@server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { response } from "@server/utils";
|
import { response } from "@server/utils";
|
||||||
import { verifyPassword } from "./password";
|
import { verifyPassword } from "./password";
|
||||||
import { verifyTotpCode } from "./verifyTotpCode";
|
import { verifyTotpCode } from "./2fa";
|
||||||
|
|
||||||
export const disable2faBody = z.object({
|
export const disable2faBody = z.object({
|
||||||
password: z.string(),
|
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) {
|
if (!validOTP) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -2,19 +2,14 @@ import { verify } from "@node-rs/argon2";
|
||||||
import lucia, { verifySession } from "@server/auth";
|
import lucia, { verifySession } from "@server/auth";
|
||||||
import db from "@server/db";
|
import db from "@server/db";
|
||||||
import { users } from "@server/db/schema";
|
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 HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { decodeHex } from "oslo/encoding";
|
|
||||||
import { TOTPController } from "oslo/otp";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { verifyTotpCode } from "./verifyTotpCode";
|
import { verifyTotpCode } from "./2fa";
|
||||||
|
|
||||||
export const loginBodySchema = z.object({
|
export const loginBodySchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
@ -108,6 +103,7 @@ export async function login(
|
||||||
const validOTP = await verifyTotpCode(
|
const validOTP = await verifyTotpCode(
|
||||||
code,
|
code,
|
||||||
existingUser.twoFactorSecret!,
|
existingUser.twoFactorSecret!,
|
||||||
|
existingUser.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!validOTP) {
|
if (!validOTP) {
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { Request, Response, NextFunction } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { decodeHex } from "oslo/encoding";
|
|
||||||
import { TOTPController } from "oslo/otp";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { response } from "@server/utils";
|
import { response } from "@server/utils";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
|
@ -11,6 +9,7 @@ import { twoFactorBackupCodes, User, users } from "@server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { alphabet, generateRandomString } from "oslo/crypto";
|
import { alphabet, generateRandomString } from "oslo/crypto";
|
||||||
import { hashPassword } from "./password";
|
import { hashPassword } from "./password";
|
||||||
|
import { verifyTotpCode } from "./2fa";
|
||||||
|
|
||||||
export const verifyTotpBody = z.object({
|
export const verifyTotpBody = z.object({
|
||||||
code: z.string(),
|
code: z.string(),
|
||||||
|
@ -62,11 +61,7 @@ export async function verifyTotp(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const totpController = new TOTPController();
|
const valid = await verifyTotpCode(code, user.twoFactorSecret, user.id);
|
||||||
const valid = await totpController.verify(
|
|
||||||
code,
|
|
||||||
decodeHex(user.twoFactorSecret),
|
|
||||||
);
|
|
||||||
|
|
||||||
const backupCodes = await generateBackupCodes();
|
const backupCodes = await generateBackupCodes();
|
||||||
for (const code of backupCodes) {
|
for (const code of backupCodes) {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue