From 1f1862e526c4e44eb09800b604ff0c76ad8a019c Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Sun, 9 Mar 2025 11:40:27 -0400 Subject: [PATCH] chore: migrate two-factor APIs to app router (#19826) * wip * remove comment * clean up * clean up --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> --- .../auth/two-factor/totp/disable/route.ts} | 57 ++++++++-------- .../api/auth/two-factor/totp/enable/route.ts | 68 +++++++++++++++++++ .../api/auth/two-factor/totp/setup/route.ts} | 37 +++++----- .../pages/api/auth/two-factor/totp/enable.ts | 66 ------------------ 4 files changed, 117 insertions(+), 111 deletions(-) rename apps/web/{pages/api/auth/two-factor/totp/disable.ts => app/api/auth/two-factor/totp/disable/route.ts} (58%) create mode 100644 apps/web/app/api/auth/two-factor/totp/enable/route.ts rename apps/web/{pages/api/auth/two-factor/totp/setup.ts => app/api/auth/two-factor/totp/setup/route.ts} (59%) delete mode 100644 apps/web/pages/api/auth/two-factor/totp/enable.ts diff --git a/apps/web/pages/api/auth/two-factor/totp/disable.ts b/apps/web/app/api/auth/two-factor/totp/disable/route.ts similarity index 58% rename from apps/web/pages/api/auth/two-factor/totp/disable.ts rename to apps/web/app/api/auth/two-factor/totp/disable/route.ts index b72f282e42..b7a229fcea 100644 --- a/apps/web/pages/api/auth/two-factor/totp/disable.ts +++ b/apps/web/app/api/auth/two-factor/totp/disable/route.ts @@ -1,4 +1,6 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import { cookies, headers } from "next/headers"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; @@ -8,68 +10,68 @@ import { totpAuthenticatorCheck } from "@calcom/lib/totp"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/client"; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== "POST") { - return res.status(405).json({ message: "Method not allowed" }); - } +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) }); - const session = await getServerSession({ req }); if (!session) { - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); } if (!session.user?.id) { console.error("Session is missing a user id."); - return res.status(500).json({ error: ErrorCode.InternalServerError }); + return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); } const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } }); + if (!user) { console.error(`Session references user that no longer exists.`); - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); } if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) { - return res.status(400).json({ error: ErrorCode.UserMissingPassword }); + return NextResponse.json({ error: ErrorCode.UserMissingPassword }, { status: 400 }); } if (!user.twoFactorEnabled) { - return res.json({ message: "Two factor disabled" }); + return NextResponse.json({ message: "Two factor disabled" }); } if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) { - const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash); + const isCorrectPassword = await verifyPassword(body.password, user.password.hash); if (!isCorrectPassword) { - return res.status(400).json({ error: ErrorCode.IncorrectPassword }); + return NextResponse.json({ error: ErrorCode.IncorrectPassword }, { status: 400 }); } } - // if user has 2fa and using backup code - if (user.twoFactorEnabled && req.body.backupCode) { + // If user has 2FA and using backup code + if (user.twoFactorEnabled && body.backupCode) { if (!process.env.CALENDSO_ENCRYPTION_KEY) { console.error("Missing encryption key; cannot proceed with backup code login."); throw new Error(ErrorCode.InternalServerError); } if (!user.backupCodes) { - return res.status(400).json({ error: ErrorCode.MissingBackupCodes }); + return NextResponse.json({ error: ErrorCode.MissingBackupCodes }, { status: 400 }); } const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY)); // check if user-supplied code matches one - const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", "")); + const index = backupCodes.indexOf(body.backupCode.replaceAll("-", "")); if (index === -1) { - return res.status(400).json({ error: ErrorCode.IncorrectBackupCode }); + return NextResponse.json({ error: ErrorCode.IncorrectBackupCode }, { status: 400 }); } // we delete all stored backup codes at the end, no need to do this here // if user has 2fa and NOT using backup code, try totp } else if (user.twoFactorEnabled) { - if (!req.body.code) { - return res.status(400).json({ error: ErrorCode.SecondFactorRequired }); - // throw new Error(ErrorCode.SecondFactorRequired); + if (!body.code) { + return NextResponse.json({ error: ErrorCode.SecondFactorRequired }, { status: 400 }); } if (!user.twoFactorSecret) { @@ -78,7 +80,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } if (!process.env.CALENDSO_ENCRYPTION_KEY) { - console.error(`"Missing encryption key; cannot proceed with two factor login."`); + console.error("Missing encryption key; cannot proceed with two factor login."); throw new Error(ErrorCode.InternalServerError); } @@ -91,14 +93,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } // If user has 2fa enabled, check if body.code is correct - const isValidToken = totpAuthenticatorCheck(req.body.code, secret); + const isValidToken = totpAuthenticatorCheck(body.code, secret); if (!isValidToken) { - return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode }); - - // throw new Error(ErrorCode.IncorrectTwoFactorCode); + return NextResponse.json({ error: ErrorCode.IncorrectTwoFactorCode }, { status: 400 }); } } - // If it is, disable users 2fa + + // Disable 2FA await prisma.user.update({ where: { id: session.user.id, @@ -110,5 +111,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - return res.json({ message: "Two factor disabled" }); + return NextResponse.json({ message: "Two factor disabled" }); } diff --git a/apps/web/app/api/auth/two-factor/totp/enable/route.ts b/apps/web/app/api/auth/two-factor/totp/enable/route.ts new file mode 100644 index 0000000000..c604b050e9 --- /dev/null +++ b/apps/web/app/api/auth/two-factor/totp/enable/route.ts @@ -0,0 +1,68 @@ +import { cookies, headers } from "next/headers"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { symmetricDecrypt } from "@calcom/lib/crypto"; +import { totpAuthenticatorCheck } from "@calcom/lib/totp"; +import prisma from "@calcom/prisma"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) }); + + if (!session) { + return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); + } + + if (!session.user?.id) { + console.error("Session is missing a user id."); + return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); + } + + const user = await prisma.user.findUnique({ where: { id: session.user.id } }); + if (!user) { + console.error(`Session references user that no longer exists.`); + return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); + } + + if (user.twoFactorEnabled) { + return NextResponse.json({ error: ErrorCode.TwoFactorAlreadyEnabled }, { status: 400 }); + } + + if (!user.twoFactorSecret) { + return NextResponse.json({ error: ErrorCode.TwoFactorSetupRequired }, { status: 400 }); + } + + if (!process.env.CALENDSO_ENCRYPTION_KEY) { + console.error("Missing encryption key; cannot proceed with two factor setup."); + return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); + } + + const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY); + if (secret.length !== 32) { + console.error( + `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}` + ); + return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); + } + + const isValidToken = totpAuthenticatorCheck(body.code, secret); + if (!isValidToken) { + return NextResponse.json({ error: ErrorCode.IncorrectTwoFactorCode }, { status: 400 }); + } + + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + twoFactorEnabled: true, + }, + }); + + return NextResponse.json({ message: "Two-factor enabled" }); +} diff --git a/apps/web/pages/api/auth/two-factor/totp/setup.ts b/apps/web/app/api/auth/two-factor/totp/setup/route.ts similarity index 59% rename from apps/web/pages/api/auth/two-factor/totp/setup.ts rename to apps/web/app/api/auth/two-factor/totp/setup/route.ts index 29ea67469c..5fc859dfaa 100644 --- a/apps/web/pages/api/auth/two-factor/totp/setup.ts +++ b/apps/web/app/api/auth/two-factor/totp/setup/route.ts @@ -1,5 +1,7 @@ import crypto from "crypto"; -import type { NextApiRequest, NextApiResponse } from "next"; +import { cookies, headers } from "next/headers"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; import { authenticator } from "otplib"; import qrcode from "qrcode"; @@ -10,54 +12,55 @@ import { symmetricEncrypt } from "@calcom/lib/crypto"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== "POST") { - return res.status(405).json({ message: "Method not allowed" }); - } +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) }); - const session = await getServerSession({ req }); if (!session) { - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); } if (!session.user?.id) { console.error("Session is missing a user id."); - return res.status(500).json({ error: ErrorCode.InternalServerError }); + return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); } const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } }); + if (!user) { console.error(`Session references user that no longer exists.`); - return res.status(401).json({ message: "Not authenticated" }); + return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); } if (user.identityProvider !== IdentityProvider.CAL && !user.password?.hash) { - return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }); + return NextResponse.json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }, { status: 400 }); } if (!user.password?.hash) { - return res.status(400).json({ error: ErrorCode.UserMissingPassword }); + return NextResponse.json({ error: ErrorCode.UserMissingPassword }, { status: 400 }); } if (user.twoFactorEnabled) { - return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled }); + return NextResponse.json({ error: ErrorCode.TwoFactorAlreadyEnabled }, { status: 400 }); } if (!process.env.CALENDSO_ENCRYPTION_KEY) { console.error("Missing encryption key; cannot proceed with two factor setup."); - return res.status(500).json({ error: ErrorCode.InternalServerError }); + return NextResponse.json({ error: ErrorCode.InternalServerError }, { status: 500 }); } - const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash); + const isCorrectPassword = await verifyPassword(body.password, user.password.hash); if (!isCorrectPassword) { - return res.status(400).json({ error: ErrorCode.IncorrectPassword }); + return NextResponse.json({ error: ErrorCode.IncorrectPassword }, { status: 400 }); } // This generates a secret 32 characters in length. Do not modify the number of // bytes without updating the sanity checks in the enable and login endpoints. const secret = authenticator.generateSecret(20); - // generate backup codes with 10 character length + // Generate backup codes with 10 character length const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex")); await prisma.user.update({ @@ -75,5 +78,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const keyUri = authenticator.keyuri(name, "Cal", secret); const dataUri = await qrcode.toDataURL(keyUri); - return res.json({ secret, keyUri, dataUri, backupCodes }); + return NextResponse.json({ secret, keyUri, dataUri, backupCodes }); } diff --git a/apps/web/pages/api/auth/two-factor/totp/enable.ts b/apps/web/pages/api/auth/two-factor/totp/enable.ts deleted file mode 100644 index 1b29b94d2c..0000000000 --- a/apps/web/pages/api/auth/two-factor/totp/enable.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { symmetricDecrypt } from "@calcom/lib/crypto"; -import { totpAuthenticatorCheck } from "@calcom/lib/totp"; -import prisma from "@calcom/prisma"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== "POST") { - return res.status(405).json({ message: "Method not allowed" }); - } - - const session = await getServerSession({ req }); - if (!session) { - return res.status(401).json({ message: "Not authenticated" }); - } - - if (!session.user?.id) { - console.error("Session is missing a user id."); - return res.status(500).json({ error: ErrorCode.InternalServerError }); - } - - const user = await prisma.user.findUnique({ where: { id: session.user.id } }); - if (!user) { - console.error(`Session references user that no longer exists.`); - return res.status(401).json({ message: "Not authenticated" }); - } - - if (user.twoFactorEnabled) { - return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled }); - } - - if (!user.twoFactorSecret) { - return res.status(400).json({ error: ErrorCode.TwoFactorSetupRequired }); - } - - if (!process.env.CALENDSO_ENCRYPTION_KEY) { - console.error("Missing encryption key; cannot proceed with two factor setup."); - return res.status(500).json({ error: ErrorCode.InternalServerError }); - } - - const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY); - if (secret.length !== 32) { - console.error( - `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}` - ); - return res.status(500).json({ error: ErrorCode.InternalServerError }); - } - - const isValidToken = totpAuthenticatorCheck(req.body.code, secret); - if (!isValidToken) { - return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode }); - } - - await prisma.user.update({ - where: { - id: session.user.id, - }, - data: { - twoFactorEnabled: true, - }, - }); - - return res.json({ message: "Two-factor enabled" }); -}