From 67527c2f52e76725ad78719d4b0307e702bd0da1 Mon Sep 17 00:00:00 2001 From: schererleander Date: Fri, 26 Dec 2025 16:24:36 +0100 Subject: feat(2fa): implement google authenticator 2fa - add otplib and qrcode dependencies - update user model with 2fa fields - add twoFactorCode to validation schema - implement api routes for setup, enable, disable - add 2fa verification in auth flow - add 2fa management ui in settings - implement 2fa challenge in login page --- src/lib/auth.ts | 21 +++++++++++++++++++-- src/lib/validation.ts | 6 +++++- 2 files changed, 24 insertions(+), 3 deletions(-) (limited to 'src/lib') diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 0ed9d12..ad47d5f 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,7 @@ import { type NextAuthOptions } from "next-auth" import CredentialsProvider from "next-auth/providers/credentials" import bcrypt from "bcryptjs" +import { authenticator } from "otplib" import dbConnect from "./mongodb" import User from "@/model/User" import { loginSchema } from "./validation" @@ -11,7 +12,8 @@ export const authOptions: NextAuthOptions = { name: "credentials", credentials: { email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" } + password: { label: "Password", type: "password" }, + twoFactorCode: { label: "2FA Code", type: "text" } }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null @@ -19,7 +21,7 @@ export const authOptions: NextAuthOptions = { const result = loginSchema.safeParse(credentials) if (!result.success) return null - const { email, password } = result.data + const { email, password, twoFactorCode } = result.data try { await dbConnect() @@ -30,6 +32,17 @@ export const authOptions: NextAuthOptions = { const isPasswordValid = await bcrypt.compare(password, user.password) if (!isPasswordValid) return null + if (user.twoFactorEnabled) { + if (!twoFactorCode) { + throw new Error("2FA_REQUIRED") + } + + const isValid = authenticator.check(twoFactorCode, user.twoFactorSecret) + if (!isValid) { + throw new Error("Invalid 2FA Code") + } + } + return { id: user._id.toString(), email: user.email, @@ -38,6 +51,10 @@ export const authOptions: NextAuthOptions = { } } catch (error) { console.error("Auth error:", error) + // Rethrow specific 2FA errors so they reach the client + if (error instanceof Error && (error.message === "2FA_REQUIRED" || error.message === "Invalid 2FA Code")) { + throw error + } return null } } diff --git a/src/lib/validation.ts b/src/lib/validation.ts index ab9416e..bc5a440 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -30,7 +30,11 @@ export const loginSchema = z.object({ .max(254, 'Email must be at most 254 characters'), password: z .string() - .max(128, 'Password must be at most 128 characters') + .max(128, 'Password must be at most 128 characters'), + twoFactorCode: z + .string() + .length(6, 'Code must be 6 digits') + .optional() }) // Profile update schema (reusing name and email from registerSchema) -- cgit v1.3.1