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>
This commit is contained in:
Benny Joo
2025-03-09 15:40:27 +00:00
committed by GitHub
co-authored by Anik Dhabal Babu
parent 63dfc3376c
commit 1f1862e526
4 changed files with 117 additions and 111 deletions
@@ -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" });
}
@@ -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" });
}
@@ -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 });
}
@@ -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" });
}