From 2317473545f207a2ff628858610f0a02e6ada240 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 11 Mar 2023 10:45:24 +1100 Subject: [PATCH] feat(web): improve session retrieval performance (#7584) * feat(web): improve session retrieval performance Switch to using `getServerSession` which avoids a HTTP round trip to retrieve session details. Additionally, migrate deprecated `app/lib/auth` calls to to `@calcom/lib` package. * fix: update failing test and lint * Consolidates auth code in features * Update yarn.lock * Update packages/trpc/server/createContext.ts * Oopsie --------- Co-authored-by: zomars --- .../security/ChangePasswordSection.tsx | 2 +- .../security/DisableTwoFactorModal.tsx | 2 +- .../security/EnableTwoFactorModal.tsx | 2 +- .../settings/DisableTwoFactorModal.tsx | 2 +- .../settings/EnableTwoFactorModal.tsx | 2 +- apps/web/components/setup/AdminUser.tsx | 2 +- apps/web/lib/auth.ts | 45 -- apps/web/lib/core/i18n/i18n.utils.ts | 14 +- apps/web/package.json | 2 +- apps/web/pages/api/auth/[...nextauth].tsx | 658 +---------------- apps/web/pages/api/auth/changepw.ts | 10 +- apps/web/pages/api/auth/reset-password.ts | 3 +- apps/web/pages/api/auth/setup.ts | 4 +- apps/web/pages/api/auth/signup.ts | 2 +- .../pages/api/auth/two-factor/totp/disable.ts | 7 +- .../pages/api/auth/two-factor/totp/enable.ts | 6 +- .../pages/api/auth/two-factor/totp/setup.ts | 7 +- apps/web/pages/api/availability/calendar.ts | 5 +- apps/web/pages/api/book/event.ts | 8 +- apps/web/pages/api/cancel.ts | 8 +- apps/web/pages/api/integrations/[...args].ts | 4 +- apps/web/pages/api/me.ts | 4 +- apps/web/pages/api/trpc/[trpc].ts | 9 +- apps/web/pages/apps/[slug]/[...pages].tsx | 6 +- apps/web/pages/apps/index.tsx | 6 +- apps/web/pages/auth/forgot-password/index.tsx | 16 +- apps/web/pages/auth/login.tsx | 8 +- apps/web/pages/auth/setup/index.tsx | 8 +- apps/web/pages/auth/signin.tsx | 7 +- apps/web/pages/auth/sso/[provider].tsx | 6 +- apps/web/pages/event-types/[type]/index.tsx | 11 +- .../web/pages/getting-started/[[...step]].tsx | 6 +- apps/web/pages/index.tsx | 9 +- .../web/pages/settings/my-account/profile.tsx | 2 +- apps/web/pages/settings/security/password.tsx | 2 +- apps/web/pages/video/[uid].tsx | 7 +- apps/web/server/lib/ssr.ts | 7 +- apps/web/test/lib/getSchedule.test.ts | 4 + packages/features/auth/SAMLLogin.tsx | 2 +- packages/features/auth/lib/ErrorCode.ts | 16 + packages/features/auth/lib/ensureSession.ts | 13 + .../features/auth/lib/getServerSession.ts | 18 + packages/features/auth/lib/getSession.ts | 10 + packages/features/auth/lib/hashPassword.ts | 6 + .../auth/lib/identityProviderNameMap.ts | 7 + packages/features/auth/lib/isPasswordValid.ts | 30 + .../auth/lib}/next-auth-custom-adapter.ts | 2 +- .../features/auth/lib/next-auth-options.ts | 664 ++++++++++++++++++ packages/features/auth/lib/validPassword.ts | 9 + packages/features/auth/lib/verifyPassword.ts | 6 + packages/features/auth/package.json | 21 + .../lib/ImpersonationProvider.ts | 2 +- packages/features/ee/teams/api/upgrade.ts | 4 +- packages/lib/auth.ts | 97 --- packages/lib/rateLimit.ts | 2 +- packages/prisma/seed.ts | 5 +- packages/trpc/react/trpc.ts | 2 +- packages/trpc/server/createContext.ts | 20 +- packages/trpc/server/routers/viewer.tsx | 3 +- packages/trpc/server/routers/viewer/auth.tsx | 4 +- yarn.lock | 134 ++-- 61 files changed, 1034 insertions(+), 956 deletions(-) delete mode 100644 apps/web/lib/auth.ts create mode 100644 packages/features/auth/lib/ErrorCode.ts create mode 100644 packages/features/auth/lib/ensureSession.ts create mode 100644 packages/features/auth/lib/getServerSession.ts create mode 100644 packages/features/auth/lib/getSession.ts create mode 100644 packages/features/auth/lib/hashPassword.ts create mode 100644 packages/features/auth/lib/identityProviderNameMap.ts create mode 100644 packages/features/auth/lib/isPasswordValid.ts rename {apps/web/lib/auth => packages/features/auth/lib}/next-auth-custom-adapter.ts (98%) create mode 100644 packages/features/auth/lib/next-auth-options.ts create mode 100644 packages/features/auth/lib/validPassword.ts create mode 100644 packages/features/auth/lib/verifyPassword.ts create mode 100644 packages/features/auth/package.json delete mode 100644 packages/lib/auth.ts diff --git a/apps/web/components/security/ChangePasswordSection.tsx b/apps/web/components/security/ChangePasswordSection.tsx index e64d68285a..bdae03d6af 100644 --- a/apps/web/components/security/ChangePasswordSection.tsx +++ b/apps/web/components/security/ChangePasswordSection.tsx @@ -1,7 +1,7 @@ import type { SyntheticEvent } from "react"; import { useState } from "react"; -import { ErrorCode } from "@calcom/lib/auth"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, showToast } from "@calcom/ui"; diff --git a/apps/web/components/security/DisableTwoFactorModal.tsx b/apps/web/components/security/DisableTwoFactorModal.tsx index 4ec16a1446..c4543fe239 100644 --- a/apps/web/components/security/DisableTwoFactorModal.tsx +++ b/apps/web/components/security/DisableTwoFactorModal.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; -import { ErrorCode } from "@calcom/lib/auth"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui"; diff --git a/apps/web/components/security/EnableTwoFactorModal.tsx b/apps/web/components/security/EnableTwoFactorModal.tsx index ba787152a7..1a4affc72f 100644 --- a/apps/web/components/security/EnableTwoFactorModal.tsx +++ b/apps/web/components/security/EnableTwoFactorModal.tsx @@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react"; import React, { useState } from "react"; import { useForm } from "react-hook-form"; -import { ErrorCode } from "@calcom/lib/auth"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, Dialog, DialogContent, Form } from "@calcom/ui"; diff --git a/apps/web/components/settings/DisableTwoFactorModal.tsx b/apps/web/components/settings/DisableTwoFactorModal.tsx index 26c8636a4f..8b8271d017 100644 --- a/apps/web/components/settings/DisableTwoFactorModal.tsx +++ b/apps/web/components/settings/DisableTwoFactorModal.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; -import { ErrorCode } from "@calcom/lib/auth"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui"; diff --git a/apps/web/components/settings/EnableTwoFactorModal.tsx b/apps/web/components/settings/EnableTwoFactorModal.tsx index ef4060864d..241eb703d7 100644 --- a/apps/web/components/settings/EnableTwoFactorModal.tsx +++ b/apps/web/components/settings/EnableTwoFactorModal.tsx @@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react"; import React, { useState } from "react"; import { useForm } from "react-hook-form"; -import { ErrorCode } from "@calcom/lib/auth"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui"; diff --git a/apps/web/components/setup/AdminUser.tsx b/apps/web/components/setup/AdminUser.tsx index 7c213581b3..d8ba344191 100644 --- a/apps/web/components/setup/AdminUser.tsx +++ b/apps/web/components/setup/AdminUser.tsx @@ -5,7 +5,7 @@ import React from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import * as z from "zod"; -import { isPasswordValid } from "@calcom/lib/auth"; +import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; import { WEBSITE_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui"; diff --git a/apps/web/lib/auth.ts b/apps/web/lib/auth.ts deleted file mode 100644 index cf2267e208..0000000000 --- a/apps/web/lib/auth.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IdentityProvider } from "@prisma/client"; -import { compare, hash } from "bcryptjs"; -import type { Session } from "next-auth"; -import type { GetSessionParams } from "next-auth/react"; -import { getSession as getSessionInner } from "next-auth/react"; - -/** @deprecated use the one from `@calcom/lib/auth` */ -export async function hashPassword(password: string) { - const hashedPassword = await hash(password, 12); - return hashedPassword; -} -/** @deprecated use the one from `@calcom/lib/auth` */ -export async function verifyPassword(password: string, hashedPassword: string) { - const isValid = await compare(password, hashedPassword); - return isValid; -} -/** @deprecated use the one from `@calcom/lib/auth` */ -export async function getSession(options: GetSessionParams): Promise { - const session = await getSessionInner(options); - - // that these are equal are ensured in `[...nextauth]`'s callback - return session as Session | null; -} -/** @deprecated use the one from `@calcom/lib/auth` */ -export enum ErrorCode { - UserNotFound = "user-not-found", - IncorrectPassword = "incorrect-password", - UserMissingPassword = "missing-password", - TwoFactorDisabled = "two-factor-disabled", - TwoFactorAlreadyEnabled = "two-factor-already-enabled", - TwoFactorSetupRequired = "two-factor-setup-required", - SecondFactorRequired = "second-factor-required", - IncorrectTwoFactorCode = "incorrect-two-factor-code", - InternalServerError = "internal-server-error", - NewPasswordMatchesOld = "new-password-matches-old", - ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled", - RateLimitExceeded = "rate-limit-exceeded", - InvalidPassword = "invalid-password", -} -/** @deprecated use the one from `@calcom/lib/auth` */ -export const identityProviderNameMap: { [key in IdentityProvider]: string } = { - [IdentityProvider.CAL]: "Cal", - [IdentityProvider.GOOGLE]: "Google", - [IdentityProvider.SAML]: "SAML", -}; diff --git a/apps/web/lib/core/i18n/i18n.utils.ts b/apps/web/lib/core/i18n/i18n.utils.ts index d951f68149..22beb2b490 100644 --- a/apps/web/lib/core/i18n/i18n.utils.ts +++ b/apps/web/lib/core/i18n/i18n.utils.ts @@ -1,10 +1,10 @@ import parser from "accept-language-parser"; import type { IncomingMessage } from "http"; +import type { GetServerSidePropsContext } from "next"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import type { Maybe } from "@calcom/trpc/server"; -import { getSession } from "@lib/auth"; - import { i18n } from "../../../next-i18next.config"; export function getLocaleFromHeaders(req: IncomingMessage): string { @@ -15,9 +15,13 @@ export function getLocaleFromHeaders(req: IncomingMessage): string { return preferredLocale ?? i18n.defaultLocale; } -export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise => { - const prisma = (await import("@calcom/prisma")).default; - const session = await getSession({ req }); +export const getOrSetUserLocaleFromHeaders = async ( + req: GetServerSidePropsContext["req"], + res: GetServerSidePropsContext["res"] +): Promise => { + const { default: prisma } = await import("@calcom/prisma"); + + const session = await getServerSession({ req, res }); const preferredLocale = getLocaleFromHeaders(req); if (session?.user?.id) { diff --git a/apps/web/package.json b/apps/web/package.json index db0ad8a799..62bc4b8802 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -87,7 +87,7 @@ "micro": "^10.0.1", "mime-types": "^2.1.35", "next": "^13.2.1", - "next-auth": "^4.18.8", + "next-auth": "^4.20.1", "next-axiom": "^0.16.0", "next-collect": "^0.2.1", "next-i18next": "^11.3.0", diff --git a/apps/web/pages/api/auth/[...nextauth].tsx b/apps/web/pages/api/auth/[...nextauth].tsx index cadc3c7f01..dcc7a822d2 100644 --- a/apps/web/pages/api/auth/[...nextauth].tsx +++ b/apps/web/pages/api/auth/[...nextauth].tsx @@ -1,659 +1,5 @@ -import type { UserPermissionRole } from "@prisma/client"; -import { IdentityProvider } from "@prisma/client"; -import { readFileSync } from "fs"; -import Handlebars from "handlebars"; -import { SignJWT } from "jose"; -import type { Session } from "next-auth"; import NextAuth from "next-auth"; -import { encode } from "next-auth/jwt"; -import type { Provider } from "next-auth/providers"; -import CredentialsProvider from "next-auth/providers/credentials"; -import EmailProvider from "next-auth/providers/email"; -import GoogleProvider from "next-auth/providers/google"; -import type { TransportOptions } from "nodemailer"; -import nodemailer from "nodemailer"; -import { authenticator } from "otplib"; -import path from "path"; -import checkLicense from "@calcom/features/ee/common/server/checkLicense"; -import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; -import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { hostedCal, isSAMLLoginEnabled, clientSecretVerifier } from "@calcom/features/ee/sso/lib/saml"; -import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth"; -import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; -import { symmetricDecrypt } from "@calcom/lib/crypto"; -import { defaultCookies } from "@calcom/lib/default-cookies"; -import { randomString } from "@calcom/lib/random"; -import rateLimit from "@calcom/lib/rateLimit"; -import { serverConfig } from "@calcom/lib/serverConfig"; -import slugify from "@calcom/lib/slugify"; -import prisma from "@calcom/prisma"; -import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils"; +import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options"; -import CalComAdapter from "@lib/auth/next-auth-custom-adapter"; - -import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants"; - -const transporter = nodemailer.createTransport({ - ...(serverConfig.transport as TransportOptions), -} as TransportOptions); - -const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase(); - -const signJwt = async (payload: { email: string }) => { - const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY); - return new SignJWT(payload) - .setProtectedHeader({ alg: "HS256" }) - .setSubject(payload.email) - .setIssuedAt() - .setIssuer(WEBSITE_URL) - .setAudience(`${WEBSITE_URL}/auth/login`) - .setExpirationTime("2m") - .sign(secret); -}; - -const loginWithTotp = async (user: { email: string }) => - `/auth/login?totp=${await signJwt({ email: user.email })}`; - -const providers: Provider[] = [ - 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(), - }, - select: { - role: true, - id: true, - username: true, - name: true, - email: true, - metadata: true, - identityProvider: true, - password: true, - twoFactorEnabled: true, - twoFactorSecret: true, - teams: { - include: { - team: true, - }, - }, - }, - }); - - // Don't leak information about it being username or password that is invalid - if (!user) { - throw new Error(ErrorCode.IncorrectUsernamePassword); - } - - if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { - throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled); - } - - if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { - throw new Error(ErrorCode.IncorrectUsernamePassword); - } - - if (user.password) { - const isCorrectPassword = await verifyPassword(credentials.password, user.password); - if (!isCorrectPassword) { - throw new Error(ErrorCode.IncorrectUsernamePassword); - } - } - - 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); - } - } - - const limiter = rateLimit({ - intervalInMs: 60 * 1000, // 1 minute - }); - await limiter.check(10, user.email); // 10 requests per minute - // Check if the user you are logging into has any active teams - const hasActiveTeams = - user.teams.filter((m: { team: { metadata: unknown } }) => { - if (!IS_TEAM_BILLING_ENABLED) return true; - const metadata = teamMetadataSchema.safeParse(m.team.metadata); - if (metadata.success && metadata.data?.subscriptionId) return true; - return false; - }).length > 0; - - // authentication success- but does it meet the minimum password requirements? - if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) { - return { - id: user.id, - username: user.username, - email: user.email, - name: user.name, - role: "INACTIVE_ADMIN", - belongsToActiveTeam: hasActiveTeams, - }; - } - - return { - id: user.id, - username: user.username, - email: user.email, - name: user.name, - role: user.role, - belongsToActiveTeam: hasActiveTeams, - }; - }, - }), - ImpersonationProvider, -]; - -if (IS_GOOGLE_LOGIN_ENABLED) { - providers.push( - GoogleProvider({ - clientId: GOOGLE_CLIENT_ID, - clientSecret: GOOGLE_CLIENT_SECRET, - allowDangerousEmailAccountLinking: true, - }) - ); -} - -if (isSAMLLoginEnabled) { - providers.push({ - id: "saml", - name: "BoxyHQ", - type: "oauth", - version: "2.0", - checks: ["pkce", "state"], - authorization: { - url: `${WEBAPP_URL}/api/auth/saml/authorize`, - params: { - scope: "", - response_type: "code", - provider: "saml", - }, - }, - token: { - url: `${WEBAPP_URL}/api/auth/saml/token`, - params: { grant_type: "authorization_code" }, - }, - userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`, - profile: (profile) => { - return { - id: profile.id || "", - firstName: profile.firstName || "", - lastName: profile.lastName || "", - email: profile.email || "", - name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(), - email_verified: true, - }; - }, - options: { - clientId: "dummy", - clientSecret: clientSecretVerifier, - }, - allowDangerousEmailAccountLinking: true, - }); - - // Idp initiated login - providers.push( - CredentialsProvider({ - id: "saml-idp", - name: "IdP Login", - credentials: { - code: {}, - }, - async authorize(credentials) { - if (!credentials) { - return null; - } - - const { code } = credentials; - - if (!code) { - return null; - } - - const { oauthController } = await jackson(); - - // Fetch access token - const { access_token } = await oauthController.token({ - code, - grant_type: "authorization_code", - redirect_uri: `${process.env.NEXTAUTH_URL}`, - client_id: "dummy", - client_secret: clientSecretVerifier, - }); - - if (!access_token) { - return null; - } - - // Fetch user info - const userInfo = await oauthController.userInfo(access_token); - - if (!userInfo) { - return null; - } - - const { id, firstName, lastName, email } = userInfo; - - return { - id: id as unknown as number, - firstName, - lastName, - email, - name: `${firstName} ${lastName}`.trim(), - email_verified: true, - }; - }, - }) - ); -} - -if (true) { - const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates"); - providers.push( - EmailProvider({ - type: "email", - maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only - // Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify. - sendVerificationRequest: ({ identifier, url }) => { - const originalUrl = new URL(url); - const webappUrl = new URL(WEBAPP_URL); - if (originalUrl.origin !== webappUrl.origin) { - url = url.replace(originalUrl.origin, webappUrl.origin); - } - const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), { - encoding: "utf8", - }); - const emailTemplate = Handlebars.compile(emailFile); - transporter.sendMail({ - from: `${process.env.EMAIL_FROM}` || APP_NAME, - to: identifier, - subject: "Your sign-in link for " + APP_NAME, - html: emailTemplate({ - base_url: WEBAPP_URL, - signin_url: url, - email: identifier, - }), - }); - }, - }) - ); -} - -function isNumber(n: string) { - return !isNaN(parseFloat(n)) && !isNaN(+n); -} - -const calcomAdapter = CalComAdapter(prisma); - -export default NextAuth({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - adapter: calcomAdapter, - session: { - strategy: "jwt", - }, - jwt: { - // decorate the native JWT encode function - // Impl. detail: We don't pass through as this function is called with encode/decode functions. - encode: async ({ token, maxAge, secret }) => { - if (token?.sub && isNumber(token.sub)) { - const user = await prisma.user.findFirst({ - where: { id: Number(token.sub) }, - select: { metadata: true }, - }); - // if no user is found, we still don't want to crash here. - if (user) { - const metadata = userMetadata.parse(user.metadata); - if (metadata?.sessionTimeout) { - maxAge = metadata.sessionTimeout * 60; - } - } - } - return encode({ secret, token, maxAge }); - }, - }, - cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")), - pages: { - signIn: "/auth/login", - signOut: "/auth/logout", - error: "/auth/error", // Error code passed in query string as ?error= - verifyRequest: "/auth/verify", - // newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest) - }, - providers, - callbacks: { - async jwt({ token, user, account }) { - const autoMergeIdentities = async () => { - const existingUser = await prisma.user.findFirst({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - where: { email: token.email! }, - select: { - id: true, - username: true, - name: true, - email: true, - role: true, - }, - }); - - if (!existingUser) { - return token; - } - - return { - ...existingUser, - ...token, - }; - }; - if (!user) { - return await autoMergeIdentities(); - } - if (!account) { - return token; - } - if (account.type === "credentials") { - // return token if credentials,saml-idp - if (account.provider === "saml-idp") { - return token; - } - // any other credentials, add user info - return { - ...token, - id: user.id, - name: user.name, - username: user.username, - email: user.email, - role: user.role, - impersonatedByUID: user?.impersonatedByUID, - belongsToActiveTeam: user?.belongsToActiveTeam, - }; - } - - // The arguments above are from the provider so we need to look up the - // user based on those values in order to construct a JWT. - if (account.type === "oauth") { - if (!account.provider || !account.providerAccountId) { - return token; - } - const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE; - - const existingUser = await prisma.user.findFirst({ - where: { - AND: [ - { - identityProvider: idP, - }, - { - identityProviderId: account.providerAccountId as string, - }, - ], - }, - }); - - if (!existingUser) { - return await autoMergeIdentities(); - } - - return { - ...token, - id: existingUser.id, - name: existingUser.name, - username: existingUser.username, - email: existingUser.email, - role: existingUser.role, - impersonatedByUID: token.impersonatedByUID as number, - belongsToActiveTeam: token?.belongsToActiveTeam as boolean, - }; - } - - return token; - }, - async session({ session, token }) { - const hasValidLicense = await checkLicense(prisma); - const calendsoSession: Session = { - ...session, - hasValidLicense, - user: { - ...session.user, - id: token.id as number, - name: token.name, - username: token.username as string, - role: token.role as UserPermissionRole, - impersonatedByUID: token.impersonatedByUID as number, - belongsToActiveTeam: token?.belongsToActiveTeam as boolean, - }, - }; - return calendsoSession; - }, - async signIn(params) { - const { user, account, profile } = params; - - if (account?.provider === "email") { - return true; - } - - // In this case we've already verified the credentials in the authorize - // callback so we can sign the user in. - // Only if provider is not saml-idp - if (account?.provider !== "saml-idp") { - if (account?.type === "credentials") { - return true; - } - - if (account?.type !== "oauth") { - return false; - } - } - - if (!user.email) { - return false; - } - - if (!user.name) { - return false; - } - - if (account?.provider) { - let idP: IdentityProvider = IdentityProvider.GOOGLE; - if (account.provider === "saml" || account.provider === "saml-idp") { - idP = IdentityProvider.SAML; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error TODO validate email_verified key on profile - user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified; - - if (!user.email_verified) { - return "/auth/error?error=unverified-email"; - } - - // Only google oauth on this path - const existingUser = await prisma.user.findFirst({ - include: { - accounts: { - where: { - provider: idP, - }, - }, - }, - where: { - identityProvider: idP as IdentityProvider, - identityProviderId: account.providerAccountId, - }, - }); - - if (existingUser) { - // In this case there's an existing user and their email address - // hasn't changed since they last logged in. - if (existingUser.email === user.email) { - try { - // If old user without Account entry we link their google account - if (existingUser.accounts.length === 0) { - const linkAccountWithUserData = { ...account, userId: existingUser.id }; - await calcomAdapter.linkAccount(linkAccountWithUserData); - } - } catch (error) { - if (error instanceof Error) { - console.error("Error while linking account of already existing user"); - } - } - if (existingUser.twoFactorEnabled) { - return loginWithTotp(existingUser); - } else { - return true; - } - } - - // If the email address doesn't match, check if an account already exists - // with the new email address. If it does, for now we return an error. If - // not, update the email of their account and log them in. - const userWithNewEmail = await prisma.user.findFirst({ - where: { email: user.email }, - }); - - if (!userWithNewEmail) { - await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } }); - if (existingUser.twoFactorEnabled) { - return loginWithTotp(existingUser); - } else { - return true; - } - } else { - return "/auth/error?error=new-email-conflict"; - } - } - - // If there's no existing user for this identity provider and id, create - // a new account. If an account already exists with the incoming email - // address return an error for now. - const existingUserWithEmail = await prisma.user.findFirst({ - where: { email: user.email }, - }); - - if (existingUserWithEmail) { - // if self-hosted then we can allow auto-merge of identity providers if email is verified - if (!hostedCal && existingUserWithEmail.emailVerified) { - if (existingUserWithEmail.twoFactorEnabled) { - return loginWithTotp(existingUserWithEmail); - } else { - return true; - } - } - - // check if user was invited - if ( - !existingUserWithEmail.password && - !existingUserWithEmail.emailVerified && - !existingUserWithEmail.username - ) { - await prisma.user.update({ - where: { email: user.email }, - data: { - // Slugify the incoming name and append a few random characters to - // prevent conflicts for users with the same name. - username: usernameSlug(user.name), - emailVerified: new Date(Date.now()), - name: user.name, - identityProvider: idP, - identityProviderId: String(user.id), - }, - }); - - if (existingUserWithEmail.twoFactorEnabled) { - return loginWithTotp(existingUserWithEmail); - } else { - return true; - } - } - - // User signs up with email/password and then tries to login with Google/SAML using the same email - if ( - existingUserWithEmail.identityProvider === IdentityProvider.CAL && - (idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML) - ) { - await prisma.user.update({ - where: { email: existingUserWithEmail.email }, - data: { password: null }, - }); - if (existingUserWithEmail.twoFactorEnabled) { - return loginWithTotp(existingUserWithEmail); - } else { - return true; - } - } else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) { - return "/auth/error?error=use-password-login"; - } - - return "/auth/error?error=use-identity-login"; - } - - const newUser = await prisma.user.create({ - data: { - // Slugify the incoming name and append a few random characters to - // prevent conflicts for users with the same name. - username: usernameSlug(user.name), - emailVerified: new Date(Date.now()), - name: user.name, - email: user.email, - identityProvider: idP, - identityProviderId: String(user.id), - }, - }); - - const linkAccountNewUserData = { ...account, userId: newUser.id }; - await calcomAdapter.linkAccount(linkAccountNewUserData); - - if (account.twoFactorEnabled) { - return loginWithTotp(newUser); - } else { - return true; - } - } - - return false; - }, - async redirect({ url, baseUrl }) { - // Allows relative callback URLs - if (url.startsWith("/")) return `${baseUrl}${url}`; - // Allows callback URLs on the same domain - else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url; - return baseUrl; - }, - }, -}); +export default NextAuth(AUTH_OPTIONS); diff --git a/apps/web/pages/api/auth/changepw.ts b/apps/web/pages/api/auth/changepw.ts index 525bc4e8bc..e61ee01b7f 100644 --- a/apps/web/pages/api/auth/changepw.ts +++ b/apps/web/pages/api/auth/changepw.ts @@ -1,14 +1,14 @@ import { IdentityProvider } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import prisma from "@calcom/prisma"; -import { getSession } from "@lib/auth"; - -import { ErrorCode, hashPassword, verifyPassword } from "../../../lib/auth"; - export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({ req: req }); + const session = await getServerSession({ req, res }); if (!session || !session.user || !session.user.email) { res.status(401).json({ message: "Not authenticated" }); diff --git a/apps/web/pages/api/auth/reset-password.ts b/apps/web/pages/api/auth/reset-password.ts index 56504a505b..9cd81f2c7a 100644 --- a/apps/web/pages/api/auth/reset-password.ts +++ b/apps/web/pages/api/auth/reset-password.ts @@ -1,9 +1,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import prisma from "@calcom/prisma"; -import { hashPassword } from "@lib/auth"; - export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { return res.status(400).json({ message: "" }); diff --git a/apps/web/pages/api/auth/setup.ts b/apps/web/pages/api/auth/setup.ts index fc5186bd29..346b271e1a 100644 --- a/apps/web/pages/api/auth/setup.ts +++ b/apps/web/pages/api/auth/setup.ts @@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client"; import type { NextApiRequest } from "next"; import z from "zod"; -import { isPasswordValid } from "@calcom/lib/auth"; -import { hashPassword } from "@calcom/lib/auth"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; import { HttpError } from "@calcom/lib/http-error"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import slugify from "@calcom/lib/slugify"; diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts index 5d098a772f..dc6d22d5e7 100644 --- a/apps/web/pages/api/auth/signup.ts +++ b/apps/web/pages/api/auth/signup.ts @@ -1,7 +1,7 @@ import { IdentityProvider } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { hashPassword } from "@calcom/lib/auth"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import slugify from "@calcom/lib/slugify"; import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import prisma from "@calcom/prisma"; diff --git a/apps/web/pages/api/auth/two-factor/totp/disable.ts b/apps/web/pages/api/auth/two-factor/totp/disable.ts index 7dc0557225..11c3c70585 100644 --- a/apps/web/pages/api/auth/two-factor/totp/disable.ts +++ b/apps/web/pages/api/auth/two-factor/totp/disable.ts @@ -1,17 +1,18 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { authenticator } from "otplib"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import prisma from "@calcom/prisma"; -import { ErrorCode, getSession, verifyPassword } from "@lib/auth"; - 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 getSession({ req }); + const session = await getServerSession({ req, res }); if (!session) { return res.status(401).json({ message: "Not authenticated" }); } diff --git a/apps/web/pages/api/auth/two-factor/totp/enable.ts b/apps/web/pages/api/auth/two-factor/totp/enable.ts index 6f95a4b8a9..98322cc34c 100644 --- a/apps/web/pages/api/auth/two-factor/totp/enable.ts +++ b/apps/web/pages/api/auth/two-factor/totp/enable.ts @@ -1,17 +1,17 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { authenticator } from "otplib"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import prisma from "@calcom/prisma"; -import { ErrorCode, getSession } from "@lib/auth"; - 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 getSession({ req }); + const session = await getServerSession({ req, res }); if (!session) { return res.status(401).json({ message: "Not authenticated" }); } diff --git a/apps/web/pages/api/auth/two-factor/totp/setup.ts b/apps/web/pages/api/auth/two-factor/totp/setup.ts index e40f5622f8..135da4238a 100644 --- a/apps/web/pages/api/auth/two-factor/totp/setup.ts +++ b/apps/web/pages/api/auth/two-factor/totp/setup.ts @@ -3,17 +3,18 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { authenticator } from "otplib"; import qrcode from "qrcode"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import { symmetricEncrypt } from "@calcom/lib/crypto"; import prisma from "@calcom/prisma"; -import { ErrorCode, getSession, verifyPassword } from "@lib/auth"; - 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 getSession({ req }); + const session = await getServerSession({ req, res }); if (!session) { return res.status(401).json({ message: "Not authenticated" }); } diff --git a/apps/web/pages/api/availability/calendar.ts b/apps/web/pages/api/availability/calendar.ts index d5431117ce..aecbe4f51b 100644 --- a/apps/web/pages/api/availability/calendar.ts +++ b/apps/web/pages/api/availability/calendar.ts @@ -1,14 +1,13 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import notEmpty from "@calcom/lib/notEmpty"; import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache"; import prisma from "@calcom/prisma"; -import { getSession } from "@lib/auth"; - export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({ req }); + const session = await getServerSession({ req, res }); if (!session?.user?.id) { res.status(401).json({ message: "Not authenticated" }); diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index b3ec5029fb..79ac39a20c 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -1,11 +1,11 @@ -import type { NextApiRequest } from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; -import { getSession } from "@calcom/lib/auth"; import { defaultResponder } from "@calcom/lib/server"; -async function handler(req: NextApiRequest & { userId?: number }) { - const session = await getSession({ req }); +async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) { + const session = await getServerSession({ req, res }); /* To mimic API behavior and comply with types */ req.userId = session?.user?.id || -1; const booking = await handleNewBooking(req, { diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index 8fd2631758..9208efec46 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -1,11 +1,11 @@ -import type { NextApiRequest } from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; -import { getSession } from "@calcom/lib/auth"; import { defaultResponder, defaultHandler } from "@calcom/lib/server"; -async function handler(req: NextApiRequest & { userId?: number }) { - const session = await getSession({ req }); +async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) { + const session = await getServerSession({ req, res }); /* To mimic API behavior and comply with types */ req.userId = session?.user?.id || -1; return await handleCancelBooking(req); diff --git a/apps/web/pages/api/integrations/[...args].ts b/apps/web/pages/api/integrations/[...args].ts index f72574cac6..228b896125 100644 --- a/apps/web/pages/api/integrations/[...args].ts +++ b/apps/web/pages/api/integrations/[...args].ts @@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import type { Session } from "next-auth"; import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath"; -import { getSession } from "@calcom/lib/auth"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType"; import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache"; import prisma from "@calcom/prisma"; @@ -42,7 +42,7 @@ const defaultIntegrationAddHandler = async ({ const handler = async (req: NextApiRequest, res: NextApiResponse) => { // Check that user is authenticated - req.session = await getSession({ req }); + req.session = await getServerSession({ req, res }); const { args } = req.query; diff --git a/apps/web/pages/api/me.ts b/apps/web/pages/api/me.ts index 616dac9af6..986fc70bcf 100644 --- a/apps/web/pages/api/me.ts +++ b/apps/web/pages/api/me.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { getSession } from "next-auth/react"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { performance } from "@calcom/lib/server/perfObserver"; let isCold = true; @@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const prePrismaDate = performance.now(); const prisma = (await import("@calcom/prisma")).default; const preSessionDate = performance.now(); - const session = await getSession({ req }); + const session = await getServerSession({ req, res }); if (!session) return res.status(409).json({ message: "Unauthorized" }); const preUserDate = performance.now(); const user = await prisma.user.findUnique({ where: { id: session.user.id } }); diff --git a/apps/web/pages/api/trpc/[trpc].ts b/apps/web/pages/api/trpc/[trpc].ts index d00d2ebb3e..4dd7642397 100644 --- a/apps/web/pages/api/trpc/[trpc].ts +++ b/apps/web/pages/api/trpc/[trpc].ts @@ -3,8 +3,9 @@ */ import { z } from "zod"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import * as trpcNext from "@calcom/trpc/server/adapters/next"; -import { createContext } from "@calcom/trpc/server/createContext"; +import { createContext as createTrpcContext } from "@calcom/trpc/server/createContext"; import { appRouter } from "@calcom/trpc/server/routers/_app"; export default trpcNext.createNextApiHandler({ @@ -12,7 +13,11 @@ export default trpcNext.createNextApiHandler({ /** * @link https://trpc.io/docs/context */ - createContext, + createContext: ({ req, res }) => { + const sessionGetter = () => getServerSession({ req, res }); + + return createTrpcContext({ req, res }, sessionGetter); + }, /** * @link https://trpc.io/docs/error-handling */ diff --git a/apps/web/pages/apps/[slug]/[...pages].tsx b/apps/web/pages/apps/[slug]/[...pages].tsx index 5d80b85fb8..a4c502ff1c 100644 --- a/apps/web/pages/apps/[slug]/[...pages].tsx +++ b/apps/web/pages/apps/[slug]/[...pages].tsx @@ -3,11 +3,11 @@ import { useRouter } from "next/router"; import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config"; import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import prisma from "@calcom/prisma"; import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; import type { AppProps } from "@lib/app-providers"; -import { getSession } from "@lib/auth"; import { ssrInit } from "@server/lib/ssr"; @@ -103,7 +103,7 @@ export async function getServerSideProps( appPages?: string[]; }> ) { - const { params } = context; + const { params, req, res } = context; if (!params) { return { notFound: true, @@ -120,7 +120,7 @@ export async function getServerSideProps( // appPages is actually hardcoded here and no matter the fileName the same variable would be used. // We can write some validation logic later on that ensures that [...appPages].tsx file exists params.appPages = pages.slice(1); - const session = await getSession({ req: context.req }); + const session = await getServerSession({ req, res }); const user = session?.user; const result = await route.getServerSideProps( diff --git a/apps/web/pages/apps/index.tsx b/apps/web/pages/apps/index.tsx index 9ad2aba989..ba90bcd8ce 100644 --- a/apps/web/pages/apps/index.tsx +++ b/apps/web/pages/apps/index.tsx @@ -3,8 +3,8 @@ import type { ChangeEventHandler } from "react"; import { useState } from "react"; import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { classNames } from "@calcom/lib"; -import { getSession } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AppCategories } from "@calcom/prisma/client"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; @@ -86,9 +86,11 @@ export default function Apps({ categories, appStore }: inferSSRProps { + const { req, res } = context; + const ssg = await ssgInit(context); - const session = await getSession(context); + const session = await getServerSession({ req, res }); let appStore; if (session?.user?.id) { diff --git a/apps/web/pages/auth/forgot-password/index.tsx b/apps/web/pages/auth/forgot-password/index.tsx index 1ea2df7c21..99e1bfdcee 100644 --- a/apps/web/pages/auth/forgot-password/index.tsx +++ b/apps/web/pages/auth/forgot-password/index.tsx @@ -7,11 +7,10 @@ import { useRouter } from "next/router"; import type { SyntheticEvent } from "react"; import React from "react"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, EmailField } from "@calcom/ui"; -import { getSession } from "@lib/auth"; - import AuthContainer from "@components/ui/AuthContainer"; export default function ForgotPassword({ csrfToken }: { csrfToken: string }) { @@ -133,18 +132,21 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) { ); } -ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => { +export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req, res } = context; - const session = await getSession({ req }); + + const session = await getServerSession({ req, res }); if (session) { res.writeHead(302, { Location: "/" }); res.end(); - return; + return { props: {} }; } return { - csrfToken: await getCsrfToken(context), - ...(await serverSideTranslations(context.locale || "en", ["common"])), + props: { + csrfToken: await getCsrfToken(context), + ...(await serverSideTranslations(context.locale || "en", ["common"])), + }, }; }; diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index cdcec4c106..69f7a93b92 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -9,8 +9,9 @@ import { FormProvider, useForm } from "react-hook-form"; import { FaGoogle } from "react-icons/fa"; import { SAMLLogin } from "@calcom/features/auth/SAMLLogin"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; -import { ErrorCode, getSession } from "@calcom/lib/auth"; import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -220,8 +221,9 @@ export default function Login({ // TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) { - const { req } = context; - const session = await getSession({ req }); + const { req, res } = context; + + const session = await getServerSession({ req, res }); const ssr = await ssrInit(context); const verifyJwt = (jwt: string) => { diff --git a/apps/web/pages/auth/setup/index.tsx b/apps/web/pages/auth/setup/index.tsx index fd8a75ea5f..9c8d5dd4c8 100644 --- a/apps/web/pages/auth/setup/index.tsx +++ b/apps/web/pages/auth/setup/index.tsx @@ -4,8 +4,8 @@ import { useRouter } from "next/router"; import { useState } from "react"; import AdminAppsList from "@calcom/features/apps/AdminAppsList"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey"; -import { getSession } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma from "@calcom/prisma"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; @@ -136,10 +136,12 @@ export default function Setup(props: inferSSRProps) { } export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const { req, res } = context; + const ssr = await ssrInit(context); const userCount = await prisma.user.count(); - const { req } = context; - const session = await getSession({ req }); + + const session = await getServerSession({ req, res }); if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) { return { diff --git a/apps/web/pages/auth/signin.tsx b/apps/web/pages/auth/signin.tsx index 8c6f542e16..ee16a15540 100644 --- a/apps/web/pages/auth/signin.tsx +++ b/apps/web/pages/auth/signin.tsx @@ -1,6 +1,7 @@ import type { GetServerSidePropsContext } from "next"; -import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react"; +import { getProviders, signIn, getCsrfToken } from "next-auth/react"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { Button } from "@calcom/ui"; type Provider = { @@ -25,7 +26,9 @@ function signin({ providers }: { providers: Provider[] }) { export default signin; export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await getSession(context); + const { req, res } = context; + + const session = await getServerSession({ req, res }); const csrfToken = await getCsrfToken(context); const providers = await getProviders(); if (session) { diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx index ab81a482a8..b15c186ba5 100644 --- a/apps/web/pages/auth/sso/[provider].tsx +++ b/apps/web/pages/auth/sso/[provider].tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import stripe from "@calcom/features/ee/payments/server/stripe"; import { hostedCal, @@ -16,7 +17,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername"; import prisma from "@calcom/prisma"; import { asStringOrNull } from "@lib/asStringOrNull"; -import { getSession } from "@lib/auth"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import { ssrInit } from "@server/lib/ssr"; @@ -60,9 +60,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => throw new Error(`File is not named sso/[provider]`); } - const { req } = context; + const { req, res } = context; - const session = await getSession({ req }); + const session = await getServerSession({ req, res }); const ssr = await ssrInit(context); if (session) { diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 31875ac27e..6237e6c8cb 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -10,6 +10,7 @@ import { z } from "zod"; import { validateCustomEventName } from "@calcom/core/event"; import type { EventLocationType } from "@calcom/core/location"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { validateIntervalLimitOrder } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import getEventTypeById from "@calcom/lib/getEventTypeById"; @@ -26,7 +27,6 @@ import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar"; import { Form, showToast } from "@calcom/ui"; import { asStringOrThrow } from "@lib/asStringOrNull"; -import { getSession } from "@lib/auth"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import { AvailabilityTab } from "@components/eventtype/AvailabilityTab"; @@ -371,8 +371,10 @@ const EventTypePageWrapper = (props: inferSSRProps) = }; export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const { req, query } = context; - const session = await getSession({ req }); + const { req, res, query } = context; + + const session = await getServerSession({ req, res }); + const typeParam = parseInt(asStringOrThrow(query.type)); const ssr = await ssrInit(context); @@ -393,9 +395,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => try { const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma }); + return { props: { - session, + // session, type: typeParam, trpcState: ssr.dehydrate(), initialData: { diff --git a/apps/web/pages/getting-started/[[...step]].tsx b/apps/web/pages/getting-started/[[...step]].tsx index c360b4c40e..1e69000970 100644 --- a/apps/web/pages/getting-started/[[...step]].tsx +++ b/apps/web/pages/getting-started/[[...step]].tsx @@ -4,7 +4,7 @@ import Head from "next/head"; import { useRouter } from "next/router"; import { z } from "zod"; -import { getSession } from "@calcom/lib/auth"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { APP_NAME } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma from "@calcom/prisma"; @@ -140,8 +140,10 @@ const OnboardingPage = (props: IOnboardingPageProps) => { }; export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const { req, res } = context; + const crypto = await import("crypto"); - const session = await getSession(context); + const session = await getServerSession({ req, res }); if (!session?.user?.id) { return { redirect: { permanent: false, destination: "/auth/login" } }; diff --git a/apps/web/pages/index.tsx b/apps/web/pages/index.tsx index ddfb7116b9..358d5f8872 100644 --- a/apps/web/pages/index.tsx +++ b/apps/web/pages/index.tsx @@ -1,13 +1,14 @@ -import type { NextPageContext } from "next"; +import type { GetServerSidePropsContext } from "next"; -import { getSession } from "@lib/auth"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; function RedirectPage() { return; } -export async function getServerSideProps(context: NextPageContext) { - const session = await getSession(context); +export async function getServerSideProps({ req, res }: GetServerSidePropsContext) { + const session = await getServerSession({ req, res }); + if (!session?.user?.id) { return { redirect: { permanent: false, destination: "/auth/login" } }; } diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index 5b695688d5..9b580dc316 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -5,8 +5,8 @@ import type { BaseSyntheticEvent } from "react"; import { useRef, useState } from "react"; import { Controller, useForm } from "react-hook-form"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import { ErrorCode } from "@calcom/lib/auth"; import { APP_NAME } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; diff --git a/apps/web/pages/settings/security/password.tsx b/apps/web/pages/settings/security/password.tsx index a81cde94fc..76b32e6766 100644 --- a/apps/web/pages/settings/security/password.tsx +++ b/apps/web/pages/settings/security/password.tsx @@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client"; import { signOut, useSession } from "next-auth/react"; import { useForm } from "react-hook-form"; +import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import { identityProviderNameMap } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { userMetadata } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; diff --git a/apps/web/pages/video/[uid].tsx b/apps/web/pages/video/[uid].tsx index bf8b57917e..957e4a062a 100644 --- a/apps/web/pages/video/[uid].tsx +++ b/apps/web/pages/video/[uid].tsx @@ -1,9 +1,9 @@ import DailyIframe from "@daily-co/daily-js"; import type { GetServerSidePropsContext } from "next"; -import { getSession } from "next-auth/react"; import Head from "next/head"; import { useEffect } from "react"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; @@ -82,6 +82,8 @@ export default function JoinCall(props: JoinCallPageProps) { } export async function getServerSideProps(context: GetServerSidePropsContext) { + const { req, res } = context; + const ssr = await ssrInit(context); const booking = await prisma.booking.findUnique({ @@ -139,7 +141,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { startTime: booking.startTime.toString(), endTime: booking.endTime.toString(), }); - const session = await getSession(); + + const session = await getServerSession({ req, res }); // set meetingPassword to null for guests if (session?.user.id !== bookingObj.user?.id) { diff --git a/apps/web/server/lib/ssr.ts b/apps/web/server/lib/ssr.ts index a177615943..0d0c4177d4 100644 --- a/apps/web/server/lib/ssr.ts +++ b/apps/web/server/lib/ssr.ts @@ -1,6 +1,7 @@ import type { GetServerSidePropsContext } from "next"; import superjson from "superjson"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { createProxySSGHelpers } from "@calcom/trpc/react/ssg"; import { createContext } from "@calcom/trpc/server/createContext"; import { appRouter } from "@calcom/trpc/server/routers/_app"; @@ -12,7 +13,11 @@ import { appRouter } from "@calcom/trpc/server/routers/_app"; * Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end. */ export async function ssrInit(context: GetServerSidePropsContext) { - const ctx = await createContext(context); + const { req, res } = context; + + const sessionGetter = () => getServerSession({ req, res }); + + const ctx = await createContext(context, sessionGetter); const ssr = createProxySSGHelpers({ router: appRouter, diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index a99c48fc2d..9c31440361 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -1,3 +1,7 @@ +/** + * !: Stops the `jose` dependency from bundling the browser version and breaking tests + * @jest-environment node + */ import type { EventType as PrismaEventType, User as PrismaUser, diff --git a/packages/features/auth/SAMLLogin.tsx b/packages/features/auth/SAMLLogin.tsx index 5d34f5f2ea..0bc70fb3ce 100644 --- a/packages/features/auth/SAMLLogin.tsx +++ b/packages/features/auth/SAMLLogin.tsx @@ -1,5 +1,5 @@ import { signIn } from "next-auth/react"; -import { Dispatch, SetStateAction } from "react"; +import type { Dispatch, SetStateAction } from "react"; import { useFormContext } from "react-hook-form"; import z from "zod"; diff --git a/packages/features/auth/lib/ErrorCode.ts b/packages/features/auth/lib/ErrorCode.ts new file mode 100644 index 0000000000..2c6ea243fe --- /dev/null +++ b/packages/features/auth/lib/ErrorCode.ts @@ -0,0 +1,16 @@ +export enum ErrorCode { + IncorrectUsernamePassword = "incorrect-username-password", + UserNotFound = "user-not-found", + IncorrectPassword = "incorrect-password", + UserMissingPassword = "missing-password", + TwoFactorDisabled = "two-factor-disabled", + TwoFactorAlreadyEnabled = "two-factor-already-enabled", + TwoFactorSetupRequired = "two-factor-setup-required", + SecondFactorRequired = "second-factor-required", + IncorrectTwoFactorCode = "incorrect-two-factor-code", + InternalServerError = "internal-server-error", + NewPasswordMatchesOld = "new-password-matches-old", + ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled", + RateLimitExceeded = "rate-limit-exceeded", + SocialIdentityProviderRequired = "social-identity-provider-required", +} diff --git a/packages/features/auth/lib/ensureSession.ts b/packages/features/auth/lib/ensureSession.ts new file mode 100644 index 0000000000..7ddb232277 --- /dev/null +++ b/packages/features/auth/lib/ensureSession.ts @@ -0,0 +1,13 @@ +import type { NextApiRequest } from "next"; + +import { HttpError } from "@calcom/lib/http-error"; + +import { getSession } from "./getSession"; + +type CtxOrReq = { req: NextApiRequest; ctx?: never } | { ctx: { req: NextApiRequest }; req?: never }; + +export const ensureSession = async (ctxOrReq: CtxOrReq) => { + const session = await getSession(ctxOrReq); + if (!session?.user.id) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); + return session; +}; diff --git a/packages/features/auth/lib/getServerSession.ts b/packages/features/auth/lib/getServerSession.ts new file mode 100644 index 0000000000..6b8308c4e6 --- /dev/null +++ b/packages/features/auth/lib/getServerSession.ts @@ -0,0 +1,18 @@ +import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"; +import type { AuthOptions, Session } from "next-auth"; +import { getServerSession as getServerSessionInner } from "next-auth/next"; + +import { AUTH_OPTIONS } from "./next-auth-options"; + +export async function getServerSession(options: { + req: NextApiRequest | GetServerSidePropsContext["req"]; + res: NextApiResponse | GetServerSidePropsContext["res"]; + authOptions?: AuthOptions; +}) { + const { req, res, authOptions = AUTH_OPTIONS } = options; + + const session = await getServerSessionInner(req, res, authOptions); + + // that these are equal are ensured in `[...nextauth]`'s callback + return session as Session | null; +} diff --git a/packages/features/auth/lib/getSession.ts b/packages/features/auth/lib/getSession.ts new file mode 100644 index 0000000000..79aa8b0f66 --- /dev/null +++ b/packages/features/auth/lib/getSession.ts @@ -0,0 +1,10 @@ +import type { Session } from "next-auth"; +import type { GetSessionParams } from "next-auth/react"; +import { getSession as getSessionInner } from "next-auth/react"; + +export async function getSession(options: GetSessionParams): Promise { + const session = await getSessionInner(options); + + // that these are equal are ensured in `[...nextauth]`'s callback + return session as Session | null; +} diff --git a/packages/features/auth/lib/hashPassword.ts b/packages/features/auth/lib/hashPassword.ts new file mode 100644 index 0000000000..514b81aa5c --- /dev/null +++ b/packages/features/auth/lib/hashPassword.ts @@ -0,0 +1,6 @@ +import { hash } from "bcryptjs"; + +export async function hashPassword(password: string) { + const hashedPassword = await hash(password, 12); + return hashedPassword; +} diff --git a/packages/features/auth/lib/identityProviderNameMap.ts b/packages/features/auth/lib/identityProviderNameMap.ts new file mode 100644 index 0000000000..d7e9b02f4a --- /dev/null +++ b/packages/features/auth/lib/identityProviderNameMap.ts @@ -0,0 +1,7 @@ +import { IdentityProvider } from "@prisma/client"; + +export const identityProviderNameMap: { [key in IdentityProvider]: string } = { + [IdentityProvider.CAL]: "Cal", + [IdentityProvider.GOOGLE]: "Google", + [IdentityProvider.SAML]: "SAML", +}; diff --git a/packages/features/auth/lib/isPasswordValid.ts b/packages/features/auth/lib/isPasswordValid.ts new file mode 100644 index 0000000000..95f431a5f7 --- /dev/null +++ b/packages/features/auth/lib/isPasswordValid.ts @@ -0,0 +1,30 @@ +export function isPasswordValid(password: string): boolean; +export function isPasswordValid( + password: string, + breakdown: boolean, + strict?: boolean +): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean }; +export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) { + let cap = false, // Has uppercase characters + low = false, // Has lowercase characters + num = false, // At least one number + min = false, // Eight characters, or fifteen in strict mode. + admin_min = false; + if (password.length > 7 && (!strict || password.length > 14)) min = true; + if (strict && password.length > 14) admin_min = true; + for (let i = 0; i < password.length; i++) { + if (!isNaN(parseInt(password[i]))) num = true; + else { + if (password[i] === password[i].toUpperCase()) cap = true; + if (password[i] === password[i].toLowerCase()) low = true; + } + } + + if (!breakdown) return cap && low && num && min && (strict ? admin_min : true); + + let errors: Record = { caplow: cap && low, num, min }; + // Only return the admin key if strict mode is enabled. + if (strict) errors = { ...errors, admin_min }; + + return errors; +} diff --git a/apps/web/lib/auth/next-auth-custom-adapter.ts b/packages/features/auth/lib/next-auth-custom-adapter.ts similarity index 98% rename from apps/web/lib/auth/next-auth-custom-adapter.ts rename to packages/features/auth/lib/next-auth-custom-adapter.ts index 2e18430cb0..4d5ff9f32a 100644 --- a/apps/web/lib/auth/next-auth-custom-adapter.ts +++ b/packages/features/auth/lib/next-auth-custom-adapter.ts @@ -8,7 +8,7 @@ import type { } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; -import { identityProviderNameMap } from "@lib/auth"; +import { identityProviderNameMap } from "./identityProviderNameMap"; /** @return { import("next-auth/adapters").Adapter } */ export default function CalComAdapter(prismaClient: PrismaClient) { diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts new file mode 100644 index 0000000000..c1f7e845f9 --- /dev/null +++ b/packages/features/auth/lib/next-auth-options.ts @@ -0,0 +1,664 @@ +import type { UserPermissionRole } from "@prisma/client"; +import { IdentityProvider } from "@prisma/client"; +import { readFileSync } from "fs"; +import Handlebars from "handlebars"; +import { SignJWT } from "jose"; +import type { AuthOptions, Session } from "next-auth"; +import { encode } from "next-auth/jwt"; +import type { Provider } from "next-auth/providers"; +import CredentialsProvider from "next-auth/providers/credentials"; +import EmailProvider from "next-auth/providers/email"; +import GoogleProvider from "next-auth/providers/google"; +import type { TransportOptions } from "nodemailer"; +import nodemailer from "nodemailer"; +import { authenticator } from "otplib"; +import path from "path"; + +import checkLicense from "@calcom/features/ee/common/server/checkLicense"; +import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; +import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; +import { symmetricDecrypt } from "@calcom/lib/crypto"; +import { defaultCookies } from "@calcom/lib/default-cookies"; +import { randomString } from "@calcom/lib/random"; +import rateLimit from "@calcom/lib/rateLimit"; +import { serverConfig } from "@calcom/lib/serverConfig"; +import slugify from "@calcom/lib/slugify"; +import prisma from "@calcom/prisma"; +import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils"; + +import { ErrorCode } from "./ErrorCode"; +import { isPasswordValid } from "./isPasswordValid"; +import CalComAdapter from "./next-auth-custom-adapter"; +import { verifyPassword } from "./verifyPassword"; + +const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}"; +const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } = + JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {}; +const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true"; +const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED); + +const transporter = nodemailer.createTransport({ + ...(serverConfig.transport as TransportOptions), +} as TransportOptions); + +const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase(); + +const signJwt = async (payload: { email: string }) => { + const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY); + return new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setSubject(payload.email) + .setIssuedAt() + .setIssuer(WEBSITE_URL) + .setAudience(`${WEBSITE_URL}/auth/login`) + .setExpirationTime("2m") + .sign(secret); +}; + +const loginWithTotp = async (user: { email: string }) => + `/auth/login?totp=${await signJwt({ email: user.email })}`; + +const providers: Provider[] = [ + 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(), + }, + select: { + role: true, + id: true, + username: true, + name: true, + email: true, + metadata: true, + identityProvider: true, + password: true, + twoFactorEnabled: true, + twoFactorSecret: true, + teams: { + include: { + team: true, + }, + }, + }, + }); + + // Don't leak information about it being username or password that is invalid + if (!user) { + throw new Error(ErrorCode.IncorrectUsernamePassword); + } + + if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { + throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled); + } + + if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { + throw new Error(ErrorCode.IncorrectUsernamePassword); + } + + if (user.password) { + const isCorrectPassword = await verifyPassword(credentials.password, user.password); + if (!isCorrectPassword) { + throw new Error(ErrorCode.IncorrectUsernamePassword); + } + } + + 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); + } + } + + const limiter = rateLimit({ + intervalInMs: 60 * 1000, // 1 minute + }); + await limiter.check(10, user.email); // 10 requests per minute + // Check if the user you are logging into has any active teams + const hasActiveTeams = + user.teams.filter((m: { team: { metadata: unknown } }) => { + if (!IS_TEAM_BILLING_ENABLED) return true; + const metadata = teamMetadataSchema.safeParse(m.team.metadata); + if (metadata.success && metadata.data?.subscriptionId) return true; + return false; + }).length > 0; + + // authentication success- but does it meet the minimum password requirements? + if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) { + return { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + role: "INACTIVE_ADMIN", + belongsToActiveTeam: hasActiveTeams, + }; + } + + return { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + role: user.role, + belongsToActiveTeam: hasActiveTeams, + }; + }, + }), + ImpersonationProvider, +]; + +if (IS_GOOGLE_LOGIN_ENABLED) { + providers.push( + GoogleProvider({ + clientId: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + allowDangerousEmailAccountLinking: true, + }) + ); +} + +if (isSAMLLoginEnabled) { + providers.push({ + id: "saml", + name: "BoxyHQ", + type: "oauth", + version: "2.0", + checks: ["pkce", "state"], + authorization: { + url: `${WEBAPP_URL}/api/auth/saml/authorize`, + params: { + scope: "", + response_type: "code", + provider: "saml", + }, + }, + token: { + url: `${WEBAPP_URL}/api/auth/saml/token`, + params: { grant_type: "authorization_code" }, + }, + userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`, + profile: (profile) => { + return { + id: profile.id || "", + firstName: profile.firstName || "", + lastName: profile.lastName || "", + email: profile.email || "", + name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(), + email_verified: true, + }; + }, + options: { + clientId: "dummy", + clientSecret: clientSecretVerifier, + }, + allowDangerousEmailAccountLinking: true, + }); + + // Idp initiated login + providers.push( + CredentialsProvider({ + id: "saml-idp", + name: "IdP Login", + credentials: { + code: {}, + }, + async authorize(credentials) { + if (!credentials) { + return null; + } + + const { code } = credentials; + + if (!code) { + return null; + } + + const { oauthController } = await jackson(); + + // Fetch access token + const { access_token } = await oauthController.token({ + code, + grant_type: "authorization_code", + redirect_uri: `${process.env.NEXTAUTH_URL}`, + client_id: "dummy", + client_secret: clientSecretVerifier, + }); + + if (!access_token) { + return null; + } + + // Fetch user info + const userInfo = await oauthController.userInfo(access_token); + + if (!userInfo) { + return null; + } + + const { id, firstName, lastName, email } = userInfo; + + return { + id: id as unknown as number, + firstName, + lastName, + email, + name: `${firstName} ${lastName}`.trim(), + email_verified: true, + }; + }, + }) + ); +} + +if (true) { + const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates"); + providers.push( + EmailProvider({ + type: "email", + maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only + // Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify. + sendVerificationRequest: ({ identifier, url }) => { + const originalUrl = new URL(url); + const webappUrl = new URL(WEBAPP_URL); + if (originalUrl.origin !== webappUrl.origin) { + url = url.replace(originalUrl.origin, webappUrl.origin); + } + const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), { + encoding: "utf8", + }); + const emailTemplate = Handlebars.compile(emailFile); + transporter.sendMail({ + from: `${process.env.EMAIL_FROM}` || APP_NAME, + to: identifier, + subject: "Your sign-in link for " + APP_NAME, + html: emailTemplate({ + base_url: WEBAPP_URL, + signin_url: url, + email: identifier, + }), + }); + }, + }) + ); +} + +function isNumber(n: string) { + return !isNaN(parseFloat(n)) && !isNaN(+n); +} + +const calcomAdapter = CalComAdapter(prisma); + +export const AUTH_OPTIONS: AuthOptions = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + adapter: calcomAdapter, + session: { + strategy: "jwt", + }, + jwt: { + // decorate the native JWT encode function + // Impl. detail: We don't pass through as this function is called with encode/decode functions. + encode: async ({ token, maxAge, secret }) => { + if (token?.sub && isNumber(token.sub)) { + const user = await prisma.user.findFirst({ + where: { id: Number(token.sub) }, + select: { metadata: true }, + }); + // if no user is found, we still don't want to crash here. + if (user) { + const metadata = userMetadata.parse(user.metadata); + if (metadata?.sessionTimeout) { + maxAge = metadata.sessionTimeout * 60; + } + } + } + return encode({ secret, token, maxAge }); + }, + }, + cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")), + pages: { + signIn: "/auth/login", + signOut: "/auth/logout", + error: "/auth/error", // Error code passed in query string as ?error= + verifyRequest: "/auth/verify", + // newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest) + }, + providers, + callbacks: { + async jwt({ token, user, account }) { + const autoMergeIdentities = async () => { + const existingUser = await prisma.user.findFirst({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + where: { email: token.email! }, + select: { + id: true, + username: true, + name: true, + email: true, + role: true, + }, + }); + + if (!existingUser) { + return token; + } + + return { + ...existingUser, + ...token, + }; + }; + if (!user) { + return await autoMergeIdentities(); + } + if (!account) { + return token; + } + if (account.type === "credentials") { + // return token if credentials,saml-idp + if (account.provider === "saml-idp") { + return token; + } + // any other credentials, add user info + return { + ...token, + id: user.id, + name: user.name, + username: user.username, + email: user.email, + role: user.role, + impersonatedByUID: user?.impersonatedByUID, + belongsToActiveTeam: user?.belongsToActiveTeam, + }; + } + + // The arguments above are from the provider so we need to look up the + // user based on those values in order to construct a JWT. + if (account.type === "oauth") { + if (!account.provider || !account.providerAccountId) { + return token; + } + const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE; + + const existingUser = await prisma.user.findFirst({ + where: { + AND: [ + { + identityProvider: idP, + }, + { + identityProviderId: account.providerAccountId as string, + }, + ], + }, + }); + + if (!existingUser) { + return await autoMergeIdentities(); + } + + return { + ...token, + id: existingUser.id, + name: existingUser.name, + username: existingUser.username, + email: existingUser.email, + role: existingUser.role, + impersonatedByUID: token.impersonatedByUID as number, + belongsToActiveTeam: token?.belongsToActiveTeam as boolean, + }; + } + + return token; + }, + async session({ session, token }) { + const hasValidLicense = await checkLicense(prisma); + const calendsoSession: Session = { + ...session, + hasValidLicense, + user: { + ...session.user, + id: token.id as number, + name: token.name, + username: token.username as string, + role: token.role as UserPermissionRole, + impersonatedByUID: token.impersonatedByUID as number, + belongsToActiveTeam: token?.belongsToActiveTeam as boolean, + }, + }; + return calendsoSession; + }, + async signIn(params) { + const { user, account, profile } = params; + + if (account?.provider === "email") { + return true; + } + + // In this case we've already verified the credentials in the authorize + // callback so we can sign the user in. + // Only if provider is not saml-idp + if (account?.provider !== "saml-idp") { + if (account?.type === "credentials") { + return true; + } + + if (account?.type !== "oauth") { + return false; + } + } + + if (!user.email) { + return false; + } + + if (!user.name) { + return false; + } + + if (account?.provider) { + let idP: IdentityProvider = IdentityProvider.GOOGLE; + if (account.provider === "saml" || account.provider === "saml-idp") { + idP = IdentityProvider.SAML; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error TODO validate email_verified key on profile + user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified; + + if (!user.email_verified) { + return "/auth/error?error=unverified-email"; + } + + // Only google oauth on this path + const existingUser = await prisma.user.findFirst({ + include: { + accounts: { + where: { + provider: idP, + }, + }, + }, + where: { + identityProvider: idP as IdentityProvider, + identityProviderId: account.providerAccountId, + }, + }); + + if (existingUser) { + // In this case there's an existing user and their email address + // hasn't changed since they last logged in. + if (existingUser.email === user.email) { + try { + // If old user without Account entry we link their google account + if (existingUser.accounts.length === 0) { + const linkAccountWithUserData = { ...account, userId: existingUser.id }; + await calcomAdapter.linkAccount(linkAccountWithUserData); + } + } catch (error) { + if (error instanceof Error) { + console.error("Error while linking account of already existing user"); + } + } + if (existingUser.twoFactorEnabled) { + return loginWithTotp(existingUser); + } else { + return true; + } + } + + // If the email address doesn't match, check if an account already exists + // with the new email address. If it does, for now we return an error. If + // not, update the email of their account and log them in. + const userWithNewEmail = await prisma.user.findFirst({ + where: { email: user.email }, + }); + + if (!userWithNewEmail) { + await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } }); + if (existingUser.twoFactorEnabled) { + return loginWithTotp(existingUser); + } else { + return true; + } + } else { + return "/auth/error?error=new-email-conflict"; + } + } + + // If there's no existing user for this identity provider and id, create + // a new account. If an account already exists with the incoming email + // address return an error for now. + const existingUserWithEmail = await prisma.user.findFirst({ + where: { email: user.email }, + }); + + if (existingUserWithEmail) { + // if self-hosted then we can allow auto-merge of identity providers if email is verified + if (!hostedCal && existingUserWithEmail.emailVerified) { + if (existingUserWithEmail.twoFactorEnabled) { + return loginWithTotp(existingUserWithEmail); + } else { + return true; + } + } + + // check if user was invited + if ( + !existingUserWithEmail.password && + !existingUserWithEmail.emailVerified && + !existingUserWithEmail.username + ) { + await prisma.user.update({ + where: { email: user.email }, + data: { + // Slugify the incoming name and append a few random characters to + // prevent conflicts for users with the same name. + username: usernameSlug(user.name), + emailVerified: new Date(Date.now()), + name: user.name, + identityProvider: idP, + identityProviderId: String(user.id), + }, + }); + + if (existingUserWithEmail.twoFactorEnabled) { + return loginWithTotp(existingUserWithEmail); + } else { + return true; + } + } + + // User signs up with email/password and then tries to login with Google/SAML using the same email + if ( + existingUserWithEmail.identityProvider === IdentityProvider.CAL && + (idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML) + ) { + await prisma.user.update({ + where: { email: existingUserWithEmail.email }, + data: { password: null }, + }); + if (existingUserWithEmail.twoFactorEnabled) { + return loginWithTotp(existingUserWithEmail); + } else { + return true; + } + } else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) { + return "/auth/error?error=use-password-login"; + } + + return "/auth/error?error=use-identity-login"; + } + + const newUser = await prisma.user.create({ + data: { + // Slugify the incoming name and append a few random characters to + // prevent conflicts for users with the same name. + username: usernameSlug(user.name), + emailVerified: new Date(Date.now()), + name: user.name, + email: user.email, + identityProvider: idP, + identityProviderId: String(user.id), + }, + }); + + const linkAccountNewUserData = { ...account, userId: newUser.id }; + await calcomAdapter.linkAccount(linkAccountNewUserData); + + if (account.twoFactorEnabled) { + return loginWithTotp(newUser); + } else { + return true; + } + } + + return false; + }, + async redirect({ url, baseUrl }) { + // Allows relative callback URLs + if (url.startsWith("/")) return `${baseUrl}${url}`; + // Allows callback URLs on the same domain + else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url; + return baseUrl; + }, + }, +}; diff --git a/packages/features/auth/lib/validPassword.ts b/packages/features/auth/lib/validPassword.ts new file mode 100644 index 0000000000..f4116bba12 --- /dev/null +++ b/packages/features/auth/lib/validPassword.ts @@ -0,0 +1,9 @@ +export function validPassword(password: string) { + if (password.length < 7) return false; + + if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) return false; + + if (!/\d+/.test(password)) return false; + + return true; +} diff --git a/packages/features/auth/lib/verifyPassword.ts b/packages/features/auth/lib/verifyPassword.ts new file mode 100644 index 0000000000..21679f95b2 --- /dev/null +++ b/packages/features/auth/lib/verifyPassword.ts @@ -0,0 +1,6 @@ +import { compare } from "bcryptjs"; + +export async function verifyPassword(password: string, hashedPassword: string) { + const isValid = await compare(password, hashedPassword); + return isValid; +} diff --git a/packages/features/auth/package.json b/packages/features/auth/package.json new file mode 100644 index 0000000000..a341cc376e --- /dev/null +++ b/packages/features/auth/package.json @@ -0,0 +1,21 @@ +{ + "name": "@calcom/feature-auth", + "sideEffects": false, + "description": "Cal.com's main auth code", + "authors": "Cal.com, Inc.", + "version": "1.0.0", + "main": "index.ts", + "dependencies": { + "bcryptjs": "^2.4.3", + "handlebars": "^4.7.7", + "jose": "^4.13.1", + "next-auth": "^4.20.1", + "nodemailer": "^6.7.8", + "otplib": "^12.0.1", + "@calcom/dayjs": "*", + "@calcom/lib": "*", + "@calcom/prisma": "*", + "@calcom/trpc": "*", + "@calcom/ui": "*" + } +} diff --git a/packages/features/ee/impersonation/lib/ImpersonationProvider.ts b/packages/features/ee/impersonation/lib/ImpersonationProvider.ts index 7a54564c20..3e49c8698b 100644 --- a/packages/features/ee/impersonation/lib/ImpersonationProvider.ts +++ b/packages/features/ee/impersonation/lib/ImpersonationProvider.ts @@ -1,8 +1,8 @@ import type { User } from "@prisma/client"; import CredentialsProvider from "next-auth/providers/credentials"; -import { getSession } from "next-auth/react"; import { z } from "zod"; +import { getSession } from "@calcom/features/auth/lib/getSession"; import prisma from "@calcom/prisma"; const teamIdschema = z.object({ diff --git a/packages/features/ee/teams/api/upgrade.ts b/packages/features/ee/teams/api/upgrade.ts index 432c42acf3..5d6270e3f6 100644 --- a/packages/features/ee/teams/api/upgrade.ts +++ b/packages/features/ee/teams/api/upgrade.ts @@ -3,8 +3,8 @@ import type Stripe from "stripe"; import { z } from "zod"; import { getRequestedSlugError } from "@calcom/app-store/stripepayment/lib/team-billing"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import stripe from "@calcom/features/ee/payments/server/stripe"; -import { getSession } from "@calcom/lib/auth"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { HttpError } from "@calcom/lib/http-error"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; @@ -64,7 +64,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { closeComUpdateTeam(prevTeam, team); } - const session = await getSession({ req }); + const session = await getServerSession({ req, res }); if (!session) return { message: "Team upgraded successfully" }; diff --git a/packages/lib/auth.ts b/packages/lib/auth.ts deleted file mode 100644 index 4eb1102e45..0000000000 --- a/packages/lib/auth.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { IdentityProvider } from "@prisma/client"; -import { compare, hash } from "bcryptjs"; -import type { NextApiRequest } from "next"; -import type { Session } from "next-auth"; -import type { GetSessionParams } from "next-auth/react"; -import { getSession as getSessionInner } from "next-auth/react"; - -import { HttpError } from "@calcom/lib/http-error"; - -export async function hashPassword(password: string) { - const hashedPassword = await hash(password, 12); - return hashedPassword; -} - -export async function verifyPassword(password: string, hashedPassword: string) { - const isValid = await compare(password, hashedPassword); - return isValid; -} - -export function validPassword(password: string) { - if (password.length < 7) return false; - - if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) return false; - - if (!/\d+/.test(password)) return false; - - return true; -} - -export async function getSession(options: GetSessionParams): Promise { - const session = await getSessionInner(options); - - // that these are equal are ensured in `[...nextauth]`'s callback - return session as Session | null; -} - -export function isPasswordValid(password: string): boolean; -export function isPasswordValid( - password: string, - breakdown: boolean, - strict?: boolean -): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean }; -export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) { - let cap = false, // Has uppercase characters - low = false, // Has lowercase characters - num = false, // At least one number - min = false, // Eight characters, or fifteen in strict mode. - admin_min = false; - if (password.length > 7 && (!strict || password.length > 14)) min = true; - if (strict && password.length > 14) admin_min = true; - for (let i = 0; i < password.length; i++) { - if (!isNaN(parseInt(password[i]))) num = true; - else { - if (password[i] === password[i].toUpperCase()) cap = true; - if (password[i] === password[i].toLowerCase()) low = true; - } - } - - if (!breakdown) return cap && low && num && min && (strict ? admin_min : true); - - let errors: Record = { caplow: cap && low, num, min }; - // Only return the admin key if strict mode is enabled. - if (strict) errors = { ...errors, admin_min }; - - return errors; -} - -type CtxOrReq = { req: NextApiRequest; ctx?: never } | { ctx: { req: NextApiRequest }; req?: never }; - -export const ensureSession = async (ctxOrReq: CtxOrReq) => { - const session = await getSession(ctxOrReq); - if (!session?.user.id) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); - return session; -}; - -export enum ErrorCode { - IncorrectUsernamePassword = "incorrect-username-password", - UserNotFound = "user-not-found", - IncorrectPassword = "incorrect-password", - UserMissingPassword = "missing-password", - TwoFactorDisabled = "two-factor-disabled", - TwoFactorAlreadyEnabled = "two-factor-already-enabled", - TwoFactorSetupRequired = "two-factor-setup-required", - SecondFactorRequired = "second-factor-required", - IncorrectTwoFactorCode = "incorrect-two-factor-code", - InternalServerError = "internal-server-error", - NewPasswordMatchesOld = "new-password-matches-old", - ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled", - RateLimitExceeded = "rate-limit-exceeded", - SocialIdentityProviderRequired = "social-identity-provider-required", -} - -export const identityProviderNameMap: { [key in IdentityProvider]: string } = { - [IdentityProvider.CAL]: "Cal", - [IdentityProvider.GOOGLE]: "Google", - [IdentityProvider.SAML]: "SAML", -}; diff --git a/packages/lib/rateLimit.ts b/packages/lib/rateLimit.ts index 69a335902a..4941842c55 100644 --- a/packages/lib/rateLimit.ts +++ b/packages/lib/rateLimit.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import cache from "memory-cache"; -import { ErrorCode } from "./auth"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; const rateLimit = (options: { intervalInMs: number }) => { return { diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index b4137f2654..b90afe96b3 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -1,11 +1,12 @@ -import { BookingStatus, MembershipRole, Prisma, UserPermissionRole } from "@prisma/client"; +import type { Prisma, UserPermissionRole } from "@prisma/client"; +import { BookingStatus, MembershipRole } from "@prisma/client"; import { uuid } from "short-uuid"; import dailyMeta from "@calcom/app-store/dailyvideo/_metadata"; import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata"; import zoomMeta from "@calcom/app-store/zoomvideo/_metadata"; import dayjs from "@calcom/dayjs"; -import { hashPassword } from "@calcom/lib/auth"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import prisma from "."; diff --git a/packages/trpc/react/trpc.ts b/packages/trpc/react/trpc.ts index 5f3b9ffc04..e05de1af77 100644 --- a/packages/trpc/react/trpc.ts +++ b/packages/trpc/react/trpc.ts @@ -1,6 +1,5 @@ import superjson from "superjson"; -import { httpBatchLink } from "../client/links/httpBatchLink"; import { httpLink } from "../client/links/httpLink"; import { loggerLink } from "../client/links/loggerLink"; import { splitLink } from "../client/links/splitLink"; @@ -8,6 +7,7 @@ import { createTRPCNext } from "../next"; // ℹ️ Type-only import: // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export import type { TRPCClientErrorLike } from "../react"; +import { httpBatchLink } from "../react"; import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server"; import type { AppRouter } from "../server/routers/_app"; diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 0088dddd59..1de2931810 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -1,8 +1,8 @@ -import type { GetServerSidePropsContext } from "next"; +import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"; import type { Session } from "next-auth"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { getSession } from "@calcom/lib/auth"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage"; import { getLocaleFromHeaders } from "@calcom/lib/i18n"; @@ -108,6 +108,15 @@ type CreateInnerContextOptions = { i18n: Awaited>; } & Partial; +export type GetSessionFn = + | ((_options: { + req: GetServerSidePropsContext["req"] | NextApiRequest; + res: GetServerSidePropsContext["res"] | NextApiResponse; + }) => Promise) + | (() => Promise); + +const DEFAULT_SESSION_GETTER: GetSessionFn = ({ req, res }) => getServerSession({ req, res }); + /** * Inner context. Will always be available in your procedures, in contrast to the outer context. * @@ -128,9 +137,12 @@ export async function createContextInner(opts: CreateInnerContextOptions) { * Creates context for an incoming request * @link https://trpc.io/docs/context */ -export const createContext = async ({ req, res }: CreateContextOptions, sessionGetter = getSession) => { +export const createContext = async ( + { req, res }: CreateContextOptions, + sessionGetter: GetSessionFn = DEFAULT_SESSION_GETTER +) => { // for API-response caching see https://trpc.io/docs/caching - const session = await sessionGetter({ req }); + const session = await sessionGetter({ req, res }); const user = await getUserFromSession({ session, req }); const locale = user?.locale ?? getLocaleFromHeaders(req); diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index c81331dd5a..d7f99aab12 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -20,10 +20,11 @@ import { } from "@calcom/core/videoClient"; import dayjs from "@calcom/dayjs"; import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails"; +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import getEnabledApps from "@calcom/lib/apps/getEnabledApps"; -import { ErrorCode, verifyPassword } from "@calcom/lib/auth"; import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; diff --git a/packages/trpc/server/routers/viewer/auth.tsx b/packages/trpc/server/routers/viewer/auth.tsx index b7a41919cb..158472a7b7 100644 --- a/packages/trpc/server/routers/viewer/auth.tsx +++ b/packages/trpc/server/routers/viewer/auth.tsx @@ -1,7 +1,9 @@ import { IdentityProvider } from "@prisma/client"; import { z } from "zod"; -import { hashPassword, validPassword, verifyPassword } from "@calcom/lib/auth"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { validPassword } from "@calcom/features/auth/lib/validPassword"; +import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import prisma from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; diff --git a/yarn.lock b/yarn.lock index 85decd4aa3..3bbc7c126e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4160,9 +4160,9 @@ "@hapi/hoek" "^9.0.0" "@headlessui/react@^1.5.0": - version "1.7.12" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.12.tgz#9ab2baa3c4f632782631e00937f9531a34033619" - integrity sha512-FhSx5V+Qp0GvbTpaxyS+ymGDDNntCacClWsk/d8Upbr19g3AsPbjfPk4+m2CgJGcuCB5Dz7LpUIOAbvQTyjL2g== + version "1.7.13" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.13.tgz#fd150b394954e9f1d86ed2340cffd1217d6e7628" + integrity sha512-9n+EQKRtD9266xIHXdY5MfiXPDfYwl7zBM7KOx2Ae3Gdgxy8QML1FkCMjq6AsOf0l6N9uvI4HcFtuFlenaldKg== dependencies: client-only "^0.0.1" @@ -5497,19 +5497,19 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-accordion@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.0.tgz#8db063b9eaeb32ca90ffec74e190dab104b56522" - integrity sha512-CNN9ZBgCK4i4SX7gFk5s8095j55DUWi85vwRNfkfBLs0QdAG5Tb4ku6sBeugCAiLvsmxw481GyNl+C3stoJVBQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz#fa1ab1b5c6a29aa75aefaf306a9e72fe3a482dbc" + integrity sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/primitive" "1.0.0" - "@radix-ui/react-collapsible" "1.0.1" - "@radix-ui/react-collection" "1.0.1" + "@radix-ui/react-collapsible" "1.0.2" + "@radix-ui/react-collection" "1.0.2" "@radix-ui/react-compose-refs" "1.0.0" "@radix-ui/react-context" "1.0.0" "@radix-ui/react-direction" "1.0.0" "@radix-ui/react-id" "1.0.0" - "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-primitive" "1.0.2" "@radix-ui/react-use-controllable-state" "1.0.0" "@radix-ui/react-arrow@1.0.0": @@ -5539,10 +5539,10 @@ "@radix-ui/react-use-callback-ref" "1.0.0" "@radix-ui/react-use-layout-effect" "1.0.0" -"@radix-ui/react-collapsible@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.1.tgz#7184312ce18e703628b9ef75cdc44b896d6f06b1" - integrity sha512-0maX4q91iYa4gjt3PsNf7dq/yqSR+HGAE8I5p54dQ6gnveS+ETWlMoijxrhmgV1k8svxpm34mQAtqIrJt4XZmA== +"@radix-ui/react-collapsible@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz#0583470c7caa8cd1ab6f606416288d19b3baf777" + integrity sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/primitive" "1.0.0" @@ -5550,7 +5550,7 @@ "@radix-ui/react-context" "1.0.0" "@radix-ui/react-id" "1.0.0" "@radix-ui/react-presence" "1.0.0" - "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-primitive" "1.0.2" "@radix-ui/react-use-controllable-state" "1.0.0" "@radix-ui/react-use-layout-effect" "1.0.0" @@ -5591,15 +5591,15 @@ "@radix-ui/react-primitive" "1.0.0" "@radix-ui/react-slot" "1.0.0" -"@radix-ui/react-collection@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5" - integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g== +"@radix-ui/react-collection@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.2.tgz#d50da00bfa2ac14585319efdbbb081d4c5a29a97" + integrity sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.0" "@radix-ui/react-context" "1.0.0" - "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-primitive" "1.0.2" "@radix-ui/react-slot" "1.0.1" "@radix-ui/react-compose-refs@0.1.0": @@ -5695,6 +5695,18 @@ "@radix-ui/react-use-callback-ref" "1.0.0" "@radix-ui/react-use-escape-keydown" "1.0.2" +"@radix-ui/react-dismissable-layer@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz#63844d8e6bbcd010a513e7176d051c3c4044e09e" + integrity sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-primitive" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.0" + "@radix-ui/react-use-escape-keydown" "1.0.2" + "@radix-ui/react-dropdown-menu@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-1.0.0.tgz#687959e1bcdd5e8eb0de406484aff28d0974c593" @@ -5810,25 +5822,25 @@ react-remove-scroll "2.5.4" "@radix-ui/react-navigation-menu@^1.0.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.1.tgz#84f24d90e6448a0c83d3431c6eefbea73dc7522e" - integrity sha512-Khgf+LwqYfUpbFAHcFPDMj6ZrWxnwCgC96liLYwE48x9YJbXGlutOWzZaSzrgl82xS+PwoPLQunfDe/i4ZITRA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.2.tgz#ca5ee35ddbc4e57a9403be92f9573435173e35c8" + integrity sha512-1fSjOAGTThYSgJ5pENG2V8we7+6KDbfbiyt66HmLTeo0W3PAmmciclm0o97VlcVZW7q5Vg2hN7Cbj4XKo5u2sw== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/primitive" "1.0.0" - "@radix-ui/react-collection" "1.0.1" + "@radix-ui/react-collection" "1.0.2" "@radix-ui/react-compose-refs" "1.0.0" "@radix-ui/react-context" "1.0.0" "@radix-ui/react-direction" "1.0.0" - "@radix-ui/react-dismissable-layer" "1.0.2" + "@radix-ui/react-dismissable-layer" "1.0.3" "@radix-ui/react-id" "1.0.0" "@radix-ui/react-presence" "1.0.0" - "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-primitive" "1.0.2" "@radix-ui/react-use-callback-ref" "1.0.0" "@radix-ui/react-use-controllable-state" "1.0.0" "@radix-ui/react-use-layout-effect" "1.0.0" "@radix-ui/react-use-previous" "1.0.0" - "@radix-ui/react-visually-hidden" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.2" "@radix-ui/react-popover@^1.0.2": version "1.0.2" @@ -5942,6 +5954,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.1" +"@radix-ui/react-primitive@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz#54e22f49ca59ba88d8143090276d50b93f8a7053" + integrity sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.1" + "@radix-ui/react-radio-group@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.0.0.tgz#16267bdfc3c1bd293a6ac1b34f3e9b484e88ebf8" @@ -5976,19 +5996,19 @@ "@radix-ui/react-use-callback-ref" "1.0.0" "@radix-ui/react-use-controllable-state" "1.0.0" -"@radix-ui/react-roving-focus@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de" - integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA== +"@radix-ui/react-roving-focus@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz#0b4f4f9bd509f4510079e9e0734a734fd17cdce3" + integrity sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/primitive" "1.0.0" - "@radix-ui/react-collection" "1.0.1" + "@radix-ui/react-collection" "1.0.2" "@radix-ui/react-compose-refs" "1.0.0" "@radix-ui/react-context" "1.0.0" "@radix-ui/react-direction" "1.0.0" "@radix-ui/react-id" "1.0.0" - "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-primitive" "1.0.2" "@radix-ui/react-use-callback-ref" "1.0.0" "@radix-ui/react-use-controllable-state" "1.0.0" @@ -6075,9 +6095,9 @@ "@radix-ui/react-use-size" "1.0.0" "@radix-ui/react-tabs@^1.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07" - integrity sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.3.tgz#8b4158160a7c6633c893c74641e929d2708e709a" + integrity sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/primitive" "1.0.0" @@ -6085,8 +6105,8 @@ "@radix-ui/react-direction" "1.0.0" "@radix-ui/react-id" "1.0.0" "@radix-ui/react-presence" "1.0.0" - "@radix-ui/react-primitive" "1.0.1" - "@radix-ui/react-roving-focus" "1.0.2" + "@radix-ui/react-primitive" "1.0.2" + "@radix-ui/react-roving-focus" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.0" "@radix-ui/react-toggle-group@^1.0.0": @@ -6254,13 +6274,13 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.0" -"@radix-ui/react-visually-hidden@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.1.tgz#9a4ac4fc97ae8d72a10e727f16b3121b5f0aa469" - integrity sha512-K1hJcCMfWfiYUibRqf3V8r5Drpyf7rh44jnrwAbdvI5iCCijilBBeyQv9SKidYNZIopMdCyR9FnIjkHxHN0FcQ== +"@radix-ui/react-visually-hidden@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.2.tgz#29b117a59ef09a984bdad12cb98d81e8350be450" + integrity sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg== dependencies: "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-primitive" "1.0.2" "@radix-ui/rect@1.0.0": version "1.0.0" @@ -20700,7 +20720,7 @@ next-api-middleware@^1.0.1: dependencies: debug "^4.3.2" -next-auth@^4.18.8: +next-auth@^4.20.1: version "4.20.1" resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.20.1.tgz#6e65c4fde14171f6ce64f05f672f80f39fc418c7" integrity sha512-ZcTUN4qzzZ/zJYgOW0hMXccpheWtAol8QOMdMts+LYRcsPGsqf2hEityyaKyECQVw1cWInb9dF3wYwI5GZdEmQ== @@ -22105,17 +22125,17 @@ playwright-core@1.25.0: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.25.0.tgz#54dc867c6c2cc5e4233905e249206a02914d14f1" integrity sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q== -playwright-core@1.31.1: - version "1.31.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.1.tgz#4deeebbb8fb73b512593fe24bea206d8fd85ff7f" - integrity sha512-JTyX4kV3/LXsvpHkLzL2I36aCdml4zeE35x+G5aPc4bkLsiRiQshU5lWeVpHFAuC8xAcbI6FDcw/8z3q2xtJSQ== +playwright-core@1.31.2: + version "1.31.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.2.tgz#debf4b215d14cb619adb7e511c164d068075b2ed" + integrity sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ== playwright@^1.29.2: - version "1.31.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.31.1.tgz#66164cdc1506bc883c7a98b44714dfea50b22d50" - integrity sha512-zKJabsIA2rvOwJ12lGTqWv4HVJzlfw2JtUvO4hAr7J8UXQZ1qEPpX20E1vcz/9fotnTkwgqp3CVdIBwptBN3Fg== + version "1.31.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.31.2.tgz#4252280586c596746122cd1fdf9f8ff6a63fa852" + integrity sha512-jpC47n2PKQNtzB7clmBuWh6ftBRS/Bt5EGLigJ9k2QAKcNeYXZkEaDH5gmvb6+AbcE0DO6GnXdbl9ogG6Eh+og== dependencies: - playwright-core "1.31.1" + playwright-core "1.31.2" pngjs@^3.0.0, pngjs@^3.3.0, pngjs@^3.3.3: version "3.4.0" @@ -26711,9 +26731,9 @@ tzdata@^1.0.30: integrity sha512-QxRODDsXS8UVxlPazCZXplqVuD6mCe7tSyDE5SCSHXv1nmrfflXxNH4pD+fjU8KJwePhWqhp6n+FpygQeGLv7w== ua-parser-js@^1.0.33: - version "1.0.33" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4" - integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ== + version "1.0.34" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.34.tgz#b33f41c415325839f354005d25a2f588be296976" + integrity sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -28523,9 +28543,9 @@ zod-prisma@^0.5.4: ts-morph "^13.0.2" zod@^3.17.3: - version "3.20.6" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.6.tgz#2f2f08ff81291d47d99e86140fedb4e0db08361a" - integrity sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA== + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== zod@^3.20.2: version "3.20.2"