import type { IdentityProvider } from "@prisma/client"; import type { NextAuthOptions } from "next-auth"; import AzureAD from "next-auth/providers/azure-ad"; import CredentialsProvider from "next-auth/providers/credentials"; import GitHubProvider from "next-auth/providers/github"; import GoogleProvider from "next-auth/providers/google"; import { prisma } from "@formbricks/database"; import { TUserNotificationSettings } from "@formbricks/types/user"; import { createAccount } from "./account/service"; import { verifyPassword } from "./auth/utils"; import { AZUREAD_CLIENT_ID, AZUREAD_CLIENT_SECRET, AZUREAD_TENANT_ID, DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_ROLE, EMAIL_VERIFICATION_DISABLED, GITHUB_ID, GITHUB_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_DISPLAY_NAME, OIDC_ISSUER, OIDC_SIGNING_ALGORITHM, } from "./constants"; import { verifyToken } from "./jwt"; import { createMembership } from "./membership/service"; import { createOrganization, getOrganization } from "./organization/service"; import { createUser, getUserByEmail, updateUser } from "./user/service"; export const authOptions: NextAuthOptions = { providers: [ CredentialsProvider({ id: "credentials", // The name to display on the sign in form (e.g. "Sign in with...") name: "Credentials", // The credentials is used to generate a suitable form on the sign in page. // You can specify whatever fields you are expecting to be submitted. // e.g. domain, username, password, 2FA token, etc. // You can pass any HTML attribute to the tag through the object. credentials: { email: { label: "Email Address", type: "email", placeholder: "Your email address", }, password: { label: "Password", type: "password", placeholder: "Your password", }, }, async authorize(credentials, _req) { let user; try { user = await prisma.user.findUnique({ where: { email: credentials?.email, }, }); } catch (e) { console.error(e); throw Error("Internal server error. Please try again later"); } if (!user || !credentials) { throw new Error("No user matches the provided credentials"); } if (!user.password) { throw new Error("No user matches the provided credentials"); } const isValid = await verifyPassword(credentials.password, user.password); if (!isValid) { throw new Error("No user matches the provided credentials"); } return { id: user.id, email: user.email, emailVerified: user.emailVerified, imageUrl: user.imageUrl, }; }, }), CredentialsProvider({ id: "token", // The name to display on the sign in form (e.g. "Sign in with...") name: "Token", // The credentials is used to generate a suitable form on the sign in page. // You can specify whatever fields you are expecting to be submitted. // e.g. domain, username, password, 2FA token, etc. // You can pass any HTML attribute to the tag through the object. credentials: { token: { label: "Verification Token", type: "string", }, }, async authorize(credentials, _req) { let user; try { if (!credentials?.token) { throw new Error("Token not found"); } const { id } = await verifyToken(credentials?.token); user = await prisma.user.findUnique({ where: { id: id, }, }); } catch (e) { console.error(e); throw new Error("Either a user does not match the provided token or the token is invalid"); } if (!user) { throw new Error("Either a user does not match the provided token or the token is invalid"); } if (user.emailVerified) { throw new Error("Email already verified"); } user = await updateUser(user.id, { emailVerified: new Date() }); return user; }, }), GitHubProvider({ clientId: GITHUB_ID || "", clientSecret: GITHUB_SECRET || "", }), GoogleProvider({ clientId: GOOGLE_CLIENT_ID || "", clientSecret: GOOGLE_CLIENT_SECRET || "", allowDangerousEmailAccountLinking: true, }), AzureAD({ clientId: AZUREAD_CLIENT_ID || "", clientSecret: AZUREAD_CLIENT_SECRET || "", tenantId: AZUREAD_TENANT_ID || "", }), { id: "openid", name: OIDC_DISPLAY_NAME || "OpenId", type: "oauth", clientId: OIDC_CLIENT_ID || "", clientSecret: OIDC_CLIENT_SECRET || "", wellKnown: `${OIDC_ISSUER}/.well-known/openid-configuration`, authorization: { params: { scope: "openid email profile" } }, idToken: true, client: { id_token_signed_response_alg: OIDC_SIGNING_ALGORITHM || "RS256", }, checks: ["pkce", "state"], profile: (profile) => { return { id: profile.sub, name: profile.name, email: profile.email, image: profile.picture, }; }, }, ], callbacks: { async jwt({ token }) { const existingUser = await getUserByEmail(token?.email!); if (!existingUser) { return token; } return { ...token, profile: { id: existingUser.id }, }; }, async session({ session, token }) { // @ts-expect-error session.user.id = token?.id; // @ts-expect-error session.user = token.profile; return session; }, async signIn({ user, account }: any) { if (account.provider === "credentials" || account.provider === "token") { if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { throw new Error("Email Verification is Pending"); } return true; } if (!user.email || account.type !== "oauth") { return false; } if (account.provider) { const provider = account.provider.toLowerCase().replace("-", "") as IdentityProvider; // check if accounts for this provider / account Id already exists const existingUserWithAccount = await prisma.user.findFirst({ include: { accounts: { where: { provider: account.provider, }, }, }, where: { identityProvider: provider, identityProviderAccountId: account.providerAccountId, }, }); if (existingUserWithAccount) { // User with this provider found // check if email still the same if (existingUserWithAccount.email === user.email) { return true; } // user seemed to change his email within the provider // check if user with this email already exist // if not found just update user with new email address // if found throw an error (TODO find better solution) const otherUserWithEmail = await getUserByEmail(user.email); if (!otherUserWithEmail) { await updateUser(existingUserWithAccount.id, { email: user.email }); return true; } throw new Error( "Looks like you updated your email somewhere else. A user with this new email exists already." ); } // There is no existing account for this identity provider / account id // check if user account with this email already exists // if user already exists throw error and request password login const existingUserWithEmail = await getUserByEmail(user.email); if (existingUserWithEmail) { throw new Error("A user with this email exists already."); } const userProfile = await createUser({ name: user.name || user.email.split("@")[0], email: user.email, emailVerified: new Date(Date.now()), identityProvider: provider, identityProviderAccountId: account.providerAccountId, }); // Default organization assignment if env variable is set if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) { // check if organization exists let organization = await getOrganization(DEFAULT_ORGANIZATION_ID); let isNewOrganization = false; if (!organization) { // create organization with id from env organization = await createOrganization({ id: DEFAULT_ORGANIZATION_ID, name: userProfile.name + "'s Organization", }); isNewOrganization = true; } const role = isNewOrganization ? "owner" : DEFAULT_ORGANIZATION_ROLE || "admin"; await createMembership(organization.id, userProfile.id, { role, accepted: true }); await createAccount({ ...account, userId: userProfile.id, }); const updatedNotificationSettings: TUserNotificationSettings = { ...userProfile.notificationSettings, alert: { ...userProfile.notificationSettings?.alert, }, unsubscribedOrganizationIds: Array.from( new Set([ ...(userProfile.notificationSettings?.unsubscribedOrganizationIds || []), organization.id, ]) ), weeklySummary: { ...userProfile.notificationSettings?.weeklySummary, }, }; await updateUser(userProfile.id, { notificationSettings: updatedNotificationSettings, }); return true; } // Without default organization assignment return true; } return true; }, }, pages: { signIn: "/auth/login", signOut: "/auth/logout", error: "/auth/login", // Error code passed in query string as ?error= }, };