import NextAuth, { Session } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { authenticator } from "otplib"; import { ErrorCode, verifyPassword } from "@lib/auth"; import { symmetricDecrypt } from "@lib/crypto"; import prisma from "@lib/prisma"; export default NextAuth({ session: { strategy: "jwt", }, secret: process.env.JWT_SECRET, pages: { signIn: "/auth/login", signOut: "/auth/logout", error: "/auth/error", // Error code passed in query string as ?error= }, providers: [ CredentialsProvider({ id: "credentials", name: "Cal.com", type: "credentials", credentials: { email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" }, password: { label: "Password", type: "password", placeholder: "Your super secure password" }, totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" }, }, async authorize(credentials) { if (!credentials) { console.error(`For some reason credentials are missing`); throw new Error(ErrorCode.InternalServerError); } const user = await prisma.user.findUnique({ where: { email: credentials.email.toLowerCase(), }, }); if (!user) { throw new Error(ErrorCode.UserNotFound); } if (!user.password) { throw new Error(ErrorCode.UserMissingPassword); } const isCorrectPassword = await verifyPassword(credentials.password, user.password); if (!isCorrectPassword) { throw new Error(ErrorCode.IncorrectPassword); } if (user.twoFactorEnabled) { if (!credentials.totpCode) { throw new Error(ErrorCode.SecondFactorRequired); } if (!user.twoFactorSecret) { console.error(`Two factor is enabled for user ${user.id} but they have no secret`); throw new Error(ErrorCode.InternalServerError); } if (!process.env.CALENDSO_ENCRYPTION_KEY) { console.error(`"Missing encryption key; cannot proceed with two factor login."`); throw new 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}` ); throw new Error(ErrorCode.InternalServerError); } const isValidToken = authenticator.check(credentials.totpCode, secret); if (!isValidToken) { throw new Error(ErrorCode.IncorrectTwoFactorCode); } } return { id: user.id, username: user.username, email: user.email, name: user.name, }; }, }), ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.username = user.username; } return token; }, async session({ session, token }) { const calendsoSession: Session = { ...session, user: { ...session.user, id: token.id as number, username: token.username as string, }, }; return calendsoSession; }, }, });