From c6defc02ec9eaea837118bcb34e37514fc54401d Mon Sep 17 00:00:00 2001 From: Flavien David Date: Tue, 5 Mar 2024 21:09:26 +0100 Subject: [PATCH] Use Auth0 as login provider (#4156) * Add auth0 sub in user model * Use Auth0 for login * :sparkles: :shirt: * Fix typeguard * Add GitHub temporary authorization callback URL * Override AUTH0_BASE_URL in front-edge --- front/components/sparkle/AppLayout.tsx | 8 +- front/lib/auth.ts | 38 ++++--- front/lib/iam/provider.ts | 64 ++++------- front/lib/iam/session.ts | 20 ++-- front/lib/iam/users.ts | 121 +++++++++++++------- front/lib/iam/workspaces.ts | 20 ++-- front/package-lock.json | 145 ++++++++++++++---------- front/package.json | 2 +- front/pages/_app.tsx | 11 +- front/pages/api/auth/[...nextauth].js | 70 ------------ front/pages/api/auth/[auth0].ts | 9 ++ front/pages/api/auth/callback/github.ts | 18 +++ front/pages/api/create-new-workspace.ts | 6 +- front/pages/api/login.ts | 10 +- front/pages/index.tsx | 78 ++++++------- front/pages/w/[wId]/join.tsx | 20 ++-- types/src/front/user.ts | 2 +- 17 files changed, 310 insertions(+), 332 deletions(-) delete mode 100644 front/pages/api/auth/[...nextauth].js create mode 100644 front/pages/api/auth/[auth0].ts create mode 100644 front/pages/api/auth/callback/github.ts diff --git a/front/components/sparkle/AppLayout.tsx b/front/components/sparkle/AppLayout.tsx index 5468d907e983..237d105e1e69 100644 --- a/front/components/sparkle/AppLayout.tsx +++ b/front/components/sparkle/AppLayout.tsx @@ -15,7 +15,6 @@ import Link from "next/link"; import type { NextRouter } from "next/router"; import { useRouter } from "next/router"; import Script from "next/script"; -import { signOut } from "next-auth/react"; import React, { Fragment, useContext, useEffect, useState } from "react"; import { CONVERSATION_PARENT_SCROLL_DIV_ID } from "@app/components/assistant/conversation/lib"; @@ -102,12 +101,7 @@ function NavigationBar({ { - void signOut({ - callbackUrl: "/", - redirect: true, - }); - }} + href="/api/auth/logout" /> diff --git a/front/lib/auth.ts b/front/lib/auth.ts index 674737b05c4a..d32d65cab73b 100644 --- a/front/lib/auth.ts +++ b/front/lib/auth.ts @@ -1,3 +1,4 @@ +import { getSession as getAuth0Session } from "@auth0/nextjs-auth0"; import type { RoleType, UserType, @@ -21,10 +22,10 @@ import type { NextApiRequest, NextApiResponse, } from "next"; -import type { Session } from "next-auth"; -import { getServerSession } from "next-auth/next"; import { isDevelopment } from "@app/lib/development"; +import type { SessionWithUser } from "@app/lib/iam/provider"; +import { isValidSession } from "@app/lib/iam/provider"; import { FeatureFlag, Key, @@ -39,7 +40,6 @@ import { FREE_TEST_PLAN_DATA } from "@app/lib/plans/free_plans"; import { isUpgraded } from "@app/lib/plans/plan_codes"; import { new_id } from "@app/lib/utils"; import logger from "@app/logger/logger"; -import { authOptions } from "@app/pages/api/auth/[...nextauth]"; const { DUST_DEVELOPMENT_WORKSPACE_ID, @@ -86,13 +86,16 @@ export class Authenticator { /** * Get a an Authenticator for the target workspace associated with the authentified user from the - * NextAuth session. + * Auth0 session. * - * @param session any NextAuth session + * @param session any Auth0 session * @param wId string target workspace id * @returns Promise */ - static async fromSession(session: any, wId: string): Promise { + static async fromSession( + session: SessionWithUser | null, + wId: string + ): Promise { const [workspace, user] = await Promise.all([ (async () => { return Workspace.findOne({ @@ -107,8 +110,7 @@ export class Authenticator { } else { return User.findOne({ where: { - provider: session.provider.provider, - providerId: session.provider.id.toString(), + auth0Sub: session.user.sub, }, }); } @@ -157,15 +159,15 @@ export class Authenticator { /** * Get a an Authenticator for the target workspace and the authentified Super User user from the - * NextAuth session. + * Auth0 session. * Super User will have `role` set to `admin` regardless of their actual role in the workspace. * - * @param session any NextAuth session + * @param session any Auth0 session * @param wId string target workspace id * @returns Promise */ static async fromSuperUserSession( - session: any, + session: SessionWithUser | null, wId: string | null ): Promise { const [workspace, user] = await Promise.all([ @@ -185,8 +187,7 @@ export class Authenticator { } else { return User.findOne({ where: { - provider: session.provider.provider, - providerId: session.provider.id.toString(), + auth0Sub: session.user.sub, }, }); } @@ -439,7 +440,7 @@ export class Authenticator { } /** - * Retrieves the NextAuth session from the request/response. + * Retrieves the Auth0 session from the request/response. * @param req NextApiRequest request object * @param res NextApiResponse response object * @returns Promise @@ -447,8 +448,13 @@ export class Authenticator { export async function getSession( req: NextApiRequest | GetServerSidePropsContext["req"], res: NextApiResponse | GetServerSidePropsContext["res"] -): Promise { - return getServerSession(req, res, authOptions); +): Promise { + const session = await getAuth0Session(req, res); + if (!session || !isValidSession(session)) { + return null; + } + + return session; } /** diff --git a/front/lib/iam/provider.ts b/front/lib/iam/provider.ts index a871c7019c69..cf0bac3577de 100644 --- a/front/lib/iam/provider.ts +++ b/front/lib/iam/provider.ts @@ -1,56 +1,42 @@ -import type { UserProviderType } from "@dust-tt/types"; +import type { Session } from "@auth0/nextjs-auth0"; -interface LegacyProvider { - provider: UserProviderType; - id: number | string; -} - -interface LegacyExternalUser { - name: string; +// This maps to the Auth0 user. +export interface ExternalUser { email: string; - image?: string; - username?: string; - email_verified?: boolean; -} + email_verified: boolean; + name: string; + nickname: string; + sub: string; + + // Google-specific fields. + family_name?: string; + given_name?: string; -interface LegacySession { - provider: LegacyProvider; - user: LegacyExternalUser; + // Always optional. + picture?: string; } -function isLegacyExternalUser(user: unknown): user is LegacyExternalUser { +function isExternalUser(user: Session["user"]): user is ExternalUser { return ( typeof user === "object" && - user !== null && "email" in user && - "name" in user + "email_verified" in user && + "name" in user && + "nickname" in user && + "sub" in user ); } -function isLegacyProvider(provider: unknown): provider is LegacyProvider { - return ( - typeof provider === "object" && - provider !== null && - "provider" in provider && - "id" in provider - ); -} - -export function isLegacySession(session: unknown): session is LegacySession { - return ( - typeof session === "object" && - session !== null && - "provider" in session && - isLegacyProvider(session.provider) && - "user" in session && - isLegacyExternalUser(session.user) - ); +function isAuth0Session(session: unknown): session is Session { + return typeof session === "object" && session !== null && "user" in session; } // We only expose generic types to ease phasing out. -export type Session = LegacySession; +export type SessionWithUser = Omit & { user: ExternalUser }; -export function isValidSession(session: unknown): session is Session { - return isLegacySession(session); +export function isValidSession( + session: Session | null +): session is SessionWithUser { + return isAuth0Session(session) && isExternalUser(session.user); } diff --git a/front/lib/iam/session.ts b/front/lib/iam/session.ts index 12122ae73300..9215eb0ee9f0 100644 --- a/front/lib/iam/session.ts +++ b/front/lib/iam/session.ts @@ -8,7 +8,7 @@ import type { ParsedUrlQuery } from "querystring"; import { Op } from "sequelize"; import { getSession } from "@app/lib/auth"; -import type { Session } from "@app/lib/iam/provider"; +import type { SessionWithUser } from "@app/lib/iam/provider"; import { isValidSession } from "@app/lib/iam/provider"; import { fetchUserFromSession, @@ -17,19 +17,15 @@ import { import { Membership, Workspace } from "@app/lib/models"; import { withGetServerSidePropsLogging } from "@app/logger/withlogging"; -export function isGoogleSession(session: any) { - return session.provider.provider === "google"; -} - /** * Retrieves the user for a given session - * @param session any NextAuth session + * @param session any Auth0 session * @returns Promise */ export async function getUserFromSession( - session: any + session: SessionWithUser | null ): Promise { - if (!session) { + if (!session || !isValidSession(session)) { return null; } @@ -100,7 +96,7 @@ export type CustomGetServerSideProps< RequireAuth extends boolean = true > = ( context: GetServerSidePropsContext, - session: RequireAuth extends true ? Session : null + session: RequireAuth extends true ? SessionWithUser : null ) => Promise>; export function makeGetServerSidePropsRequirementsWrapper< @@ -123,12 +119,14 @@ export function makeGetServerSidePropsRequirementsWrapper< redirect: { permanent: false, // TODO(2024-03-04 flav) Add support for `returnTo=`. - destination: "/", + destination: "/api/auth/login", }, }; } - const userSession = session as RequireAuth extends true ? Session : null; + const userSession = session as RequireAuth extends true + ? SessionWithUser + : null; if (enableLogging) { return withGetServerSidePropsLogging(getServerSideProps)( diff --git a/front/lib/iam/users.ts b/front/lib/iam/users.ts index 762fd2e316c0..152f23e8c89f 100644 --- a/front/lib/iam/users.ts +++ b/front/lib/iam/users.ts @@ -1,7 +1,7 @@ +import type { Session } from "@auth0/nextjs-auth0"; import type { UserProviderType } from "@dust-tt/types"; -import type { Session } from "next-auth"; -import { isGoogleSession } from "@app/lib/iam/session"; +import type { ExternalUser, SessionWithUser } from "@app/lib/iam/provider"; import { User } from "@app/lib/models/user"; import { guessFirstandLastNameFromFullName } from "@app/lib/user"; @@ -10,10 +10,10 @@ interface LegacyProviderInfo { providerId: number | string; } -async function fetchUserWithLegacyProvider({ - provider, - providerId, -}: LegacyProviderInfo) { +async function fetchUserWithLegacyProvider( + { provider, providerId }: LegacyProviderInfo, + sub: string +) { const user = await User.findOne({ where: { provider, @@ -21,30 +21,64 @@ async function fetchUserWithLegacyProvider({ }, }); - // TODO(2024-03-04 flav) Once migrating to new auth system, backfill here. + // If a legacy user is found, attach the Auth0 user ID (sub) to the existing user account. + if (user) { + await user.update({ auth0Sub: sub }); + } return user; } -export async function fetchUserFromSession(session: any) { - const { provider } = session; +async function fetchUserWithAuth0Sub(sub: string) { + const userWithAuth0 = await User.findOne({ + where: { + auth0Sub: sub, + }, + }); + + return userWithAuth0; +} + +function mapAuth0ProviderToLegacy(session: Session): LegacyProviderInfo | null { + const { user } = session; + + const [rawProvider, providerId] = user.sub.split("|"); + switch (rawProvider) { + case "google-oauth2": + return { provider: "google", providerId }; - const legacyProviderInfo: LegacyProviderInfo = { - provider: provider.provider, - providerId: provider.id, - }; + case "github": + return { provider: "github", providerId }; - return fetchUserWithLegacyProvider(legacyProviderInfo); + default: + return null; + } +} + +export async function fetchUserFromSession(session: SessionWithUser) { + const { sub } = session.user; + + const userWithAuth0 = await fetchUserWithAuth0Sub(sub); + if (userWithAuth0) { + return userWithAuth0; + } + + const legacyProviderInfo = mapAuth0ProviderToLegacy(session); + if (!legacyProviderInfo) { + return null; + } + + return fetchUserWithLegacyProvider(legacyProviderInfo, sub); } export async function maybeUpdateFromExternalUser( user: User, - externalUser: Session["user"] + externalUser: ExternalUser ) { - if (externalUser?.image && externalUser.image !== user.imageUrl) { + if (externalUser.picture && externalUser.picture !== user.imageUrl) { void User.update( { - imageUrl: externalUser.image, + imageUrl: externalUser.picture, }, { where: { @@ -55,34 +89,34 @@ export async function maybeUpdateFromExternalUser( } } -export async function createOrUpdateUser(session: any): Promise { - const user = await User.findOne({ - where: { - provider: session.provider.provider, - providerId: session.provider.id.toString(), - }, - }); - +export async function createOrUpdateUser( + session: SessionWithUser +): Promise { const { user: externalUser } = session; - // TODO(2024-03-04 flav): Remove when deprecating next-auth. - externalUser.email_verified = isGoogleSession(session); - if (user) { - // Update the user object from the updated session information. - user.username = externalUser.username; - user.name = externalUser.name; + const user = await fetchUserFromSession(session); + if (user) { // We only update the user's email if the email is verified. if (externalUser.email_verified) { user.email = externalUser.email; } + // Update the user object from the updated session information. + user.username = externalUser.nickname; + user.name = externalUser.name; + if (!user.firstName && !user.lastName) { - const { firstName, lastName } = guessFirstandLastNameFromFullName( - externalUser.name - ); - user.firstName = firstName; - user.lastName = lastName; + if (externalUser.given_name && externalUser.family_name) { + user.firstName = externalUser.given_name; + user.lastName = externalUser.family_name; + } else { + const { firstName, lastName } = guessFirstandLastNameFromFullName( + externalUser.name + ); + user.firstName = firstName; + user.lastName = lastName; + } } await user.save(); @@ -90,17 +124,16 @@ export async function createOrUpdateUser(session: any): Promise { return user; } else { const { firstName, lastName } = guessFirstandLastNameFromFullName( - session.user.name + externalUser.name ); return User.create({ - provider: session.provider.provider, - providerId: session.provider.id.toString(), - username: session.user.username, - email: session.user.email, - name: session.user.name, - firstName, - lastName, + auth0Sub: externalUser.sub, + username: externalUser.nickname, + email: externalUser.email, + name: externalUser.name, + firstName: externalUser.given_name ?? firstName, + lastName: externalUser.family_name ?? lastName, }); } } diff --git a/front/lib/iam/workspaces.ts b/front/lib/iam/workspaces.ts index 5a49210a0cbe..289fcdca87ee 100644 --- a/front/lib/iam/workspaces.ts +++ b/front/lib/iam/workspaces.ts @@ -1,22 +1,22 @@ -import { isGoogleSession } from "@app/lib/iam/session"; +import type { SessionWithUser } from "@app/lib/iam/provider"; import { Workspace, WorkspaceHasDomain } from "@app/lib/models"; import { generateModelSId } from "@app/lib/utils"; import { isDisposableEmailDomain } from "@app/lib/utils/disposable_email_domains"; -export async function createWorkspace(session: any) { - const [, emailDomain] = session.user.email.split("@"); +export async function createWorkspace(session: SessionWithUser) { + const { user: externalUser } = session; - // Use domain only when email is verified on Google Workspace and non-disposable. - const isEmailVerified = - isGoogleSession(session) && session.user.email_verified; + const [, emailDomain] = externalUser.email.split("@"); + + // Use domain only when email is verified and non-disposable. const verifiedDomain = - isEmailVerified && !isDisposableEmailDomain(emailDomain) + externalUser.email_verified && !isDisposableEmailDomain(emailDomain) ? emailDomain : null; const workspace = await Workspace.create({ sId: generateModelSId(), - name: session.user.username, + name: externalUser.nickname, }); if (verifiedDomain) { @@ -36,11 +36,11 @@ export async function createWorkspace(session: any) { } export async function findWorkspaceWithVerifiedDomain( - session: any + session: SessionWithUser ): Promise { const { user } = session; - if (!isGoogleSession(session) || !user.email_verified) { + if (!user.email_verified) { return null; } diff --git a/front/package-lock.json b/front/package-lock.json index bd96da687dfc..239a98259194 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "@amplitude/ampli": "^1.34.0", "@amplitude/analytics-node": "^1.3.5", + "@auth0/nextjs-auth0": "^3.5.0", "@dust-tt/sparkle": "^0.2.106", "@dust-tt/types": "file:../types", "@emoji-mart/data": "^1.1.2", @@ -54,7 +55,6 @@ "minimist": "^1.2.8", "moment-timezone": "^0.5.43", "next": "^13.5.6", - "next-auth": "^4.18.10", "next-remove-imports": "^1.0.8", "openai": "^4.28.0", "pdfjs-dist": "^3.7.107", @@ -9612,6 +9612,36 @@ "node": ">=6.0.0" } }, + "node_modules/@auth0/nextjs-auth0": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-3.5.0.tgz", + "integrity": "sha512-uFZEE2QQf1zU+jRK2fwqxRQt+WSqDPYF2tnr7d6BEa7b6L6tpPJ3evzoImbWSY1a7gFdvD7RD/Rvrsx7B5CKVg==", + "dependencies": { + "@panva/hkdf": "^1.0.2", + "cookie": "^0.6.0", + "debug": "^4.3.4", + "joi": "^17.6.0", + "jose": "^4.9.2", + "oauth4webapi": "^2.3.0", + "openid-client": "^5.2.1", + "tslib": "^2.4.0", + "url-join": "^4.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "next": ">=10" + } + }, + "node_modules/@auth0/nextjs-auth0/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "license": "MIT", @@ -10807,6 +10837,19 @@ "node": ">=6" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@headlessui/react": { "version": "1.7.17", "license": "MIT", @@ -12811,6 +12854,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "dev": true, @@ -20242,6 +20303,18 @@ "jiti": "bin/jiti.js" } }, + "node_modules/joi": { + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/jose": { "version": "4.15.3", "license": "MIT", @@ -22169,39 +22242,6 @@ } } }, - "node_modules/next-auth": { - "version": "4.24.5", - "license": "ISC", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@panva/hkdf": "^1.0.2", - "cookie": "^0.5.0", - "jose": "^4.11.4", - "oauth": "^0.9.15", - "openid-client": "^5.4.0", - "preact": "^10.6.3", - "preact-render-to-string": "^5.1.19", - "uuid": "^8.3.2" - }, - "peerDependencies": { - "next": "^12.2.5 || ^13 || ^14", - "nodemailer": "^6.6.5", - "react": "^17.0.2 || ^18", - "react-dom": "^17.0.2 || ^18" - }, - "peerDependenciesMeta": { - "nodemailer": { - "optional": true - } - } - }, - "node_modules/next-auth/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/next-remove-imports": { "version": "1.0.12", "license": "MIT", @@ -22413,9 +22453,13 @@ "set-blocking": "^2.0.0" } }, - "node_modules/oauth": { - "version": "0.9.15", - "license": "MIT" + "node_modules/oauth4webapi": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.3.tgz", + "integrity": "sha512-9FkXEXfzVKzH63GUOZz1zMr3wBaICSzk6DLXx+CGdrQ10ItNk2ePWzYYc1fdmKq1ayGFb2aX97sRCoZ2s0mkDw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } }, "node_modules/object-assign": { "version": "4.1.1", @@ -23582,28 +23626,6 @@ "version": "2.0.7", "license": "MIT" }, - "node_modules/preact": { - "version": "10.18.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/preact-render-to-string": { - "version": "5.2.6", - "license": "MIT", - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/preact-render-to-string/node_modules/pretty-format": { - "version": "3.8.0", - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -26659,6 +26681,11 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", diff --git a/front/package.json b/front/package.json index 33fdeb37fcb7..64c1d0e4e240 100644 --- a/front/package.json +++ b/front/package.json @@ -15,6 +15,7 @@ "dependencies": { "@amplitude/ampli": "^1.34.0", "@amplitude/analytics-node": "^1.3.5", + "@auth0/nextjs-auth0": "^3.5.0", "@dust-tt/sparkle": "^0.2.106", "@dust-tt/types": "file:../types", "@emoji-mart/data": "^1.1.2", @@ -62,7 +63,6 @@ "minimist": "^1.2.8", "moment-timezone": "^0.5.43", "next": "^13.5.6", - "next-auth": "^4.18.10", "next-remove-imports": "^1.0.8", "openai": "^4.28.0", "pdfjs-dist": "^3.7.107", diff --git a/front/pages/_app.tsx b/front/pages/_app.tsx index c327c13f652b..98a13947f88f 100644 --- a/front/pages/_app.tsx +++ b/front/pages/_app.tsx @@ -1,9 +1,9 @@ import "@app/styles/global.css"; +import { UserProvider } from "@auth0/nextjs-auth0/client"; import { SparkleContext } from "@dust-tt/sparkle"; import type { AppProps } from "next/app"; import Link from "next/link"; -import { SessionProvider } from "next-auth/react"; import type { MouseEvent, ReactNode } from "react"; import { SidebarProvider } from "@app/components/sparkle/AppLayout"; @@ -45,19 +45,16 @@ function NextLinkWrapper({ ); } -export default function App({ - Component, - pageProps: { session, ...pageProps }, -}: AppProps) { +export default function App({ Component, pageProps }: AppProps) { return ( - + - + ); } diff --git a/front/pages/api/auth/[...nextauth].js b/front/pages/api/auth/[...nextauth].js deleted file mode 100644 index 22244c5cd6bc..000000000000 --- a/front/pages/api/auth/[...nextauth].js +++ /dev/null @@ -1,70 +0,0 @@ -import NextAuth from "next-auth"; -import GithubProvider from "next-auth/providers/github"; -import GoogleProvider from "next-auth/providers/google"; - -export const authOptions = { - providers: [ - GithubProvider({ - clientId: process.env.GITHUB_ID, - clientSecret: process.env.GITHUB_SECRET, - }), - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - }), - ], - secret: process.env.NEXTAUTH_SECRET, - callbacks: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - session: async ({ session, token, user }) => { - // console.log("TOKEN", token); - // console.log("SESSION", session); - // Legacy support for old tokens. - if (token.github && !token.provider) { - token.provider = { - provider: "github", - id: token.github.id, - username: token.github.login, - access_token: token.github.access_token, - }; - } - session.provider = token.provider; - session.user.username = token.provider.username; - session.user.email_verified = !!token.provider.email_verified; - if (!session.user.image) { - session.user.image = null; - } - // console.log("FINAL SESSION", session); - return session; - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async jwt({ token, user, account, profile, isNewUser }) { - // console.log("JWT TOKEN", token); - // console.log("JWT USER", user); - // console.log("JWT ACCOUNT", account); - // console.log("JWT PROFILE", profile); - // console.log("JWT ISNEWUSER", isNewUser); - if (profile && account && account.provider === "github") { - token.provider = { - provider: "github", - id: profile.id, - username: profile.login, - access_token: account.access_token, - }; - } - if (profile && account && account.provider === "google") { - token.provider = { - provider: "google", - id: account.providerAccountId, - username: profile.email.split("@")[0], - email_verified: profile.email_verified, - access_token: account.access_token, - }; - } - // console.log("FINAL JTW TOKEN", token); - return token; - }, - }, -}; - -export default NextAuth(authOptions); diff --git a/front/pages/api/auth/[auth0].ts b/front/pages/api/auth/[auth0].ts new file mode 100644 index 000000000000..af491266e2a3 --- /dev/null +++ b/front/pages/api/auth/[auth0].ts @@ -0,0 +1,9 @@ +import { handleAuth, handleLogin } from "@auth0/nextjs-auth0"; + +export default handleAuth({ + login: handleLogin({ + authorizationParams: { + scope: "openid profile email", + }, + }), +}); diff --git a/front/pages/api/auth/callback/github.ts b/front/pages/api/auth/callback/github.ts new file mode 100644 index 000000000000..b3ac5c2e2fe0 --- /dev/null +++ b/front/pages/api/auth/callback/github.ts @@ -0,0 +1,18 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { withLogging } from "@app/logger/withlogging"; + +// The GitHub OAuth app only allows a single Authorization callback URL. +// This redirect is required during the transition to Auth0, as we deprecate the existing logic. +// It will be removed once the GitHub configuration is updated to reflect the new callback URL. +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + res.writeHead(302, { + Location: `${process.env.AUTH0_ISSUER_BASE_URL}/login/callback`, + }); + res.end(); +} + +export default withLogging(handler); diff --git a/front/pages/api/create-new-workspace.ts b/front/pages/api/create-new-workspace.ts index 735402d14038..3b1bf6aa860e 100644 --- a/front/pages/api/create-new-workspace.ts +++ b/front/pages/api/create-new-workspace.ts @@ -1,20 +1,18 @@ import type { WithAPIErrorReponse } from "@dust-tt/types"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getServerSession } from "next-auth/next"; +import { getSession } from "@app/lib/auth"; import { getUserFromSession } from "@app/lib/iam/session"; import { createWorkspace } from "@app/lib/iam/workspaces"; import { internalSubscribeWorkspaceToFreeTestPlan } from "@app/lib/plans/subscription"; import { apiError, withLogging } from "@app/logger/withlogging"; import { createAndLogMembership } from "@app/pages/api/login"; -import { authOptions } from "./auth/[...nextauth]"; - async function handler( req: NextApiRequest, res: NextApiResponse> ): Promise { - const session = await getServerSession(req, res, authOptions); + const session = await getSession(req, res); if (!session) { res.status(401).end(); return; diff --git a/front/pages/api/login.ts b/front/pages/api/login.ts index 1af7a773c485..759fc8ce036f 100644 --- a/front/pages/api/login.ts +++ b/front/pages/api/login.ts @@ -1,15 +1,15 @@ import type { WithAPIErrorReponse } from "@dust-tt/types"; import { FrontApiError } from "@dust-tt/types"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getServerSession } from "next-auth/next"; import { evaluateWorkspaceSeatAvailability } from "@app/lib/api/workspace"; -import { subscriptionForWorkspace } from "@app/lib/auth"; +import { getSession, subscriptionForWorkspace } from "@app/lib/auth"; import { getPendingMembershipInvitationForToken, markInvitationAsConsumed, } from "@app/lib/iam/invitations"; import { getActiveMembershipsForUser } from "@app/lib/iam/memberships"; +import type { SessionWithUser } from "@app/lib/iam/provider"; import { getUserFromSession } from "@app/lib/iam/session"; import { createOrUpdateUser } from "@app/lib/iam/users"; import { @@ -24,8 +24,6 @@ import { } from "@app/lib/plans/subscription"; import { apiError, withLogging } from "@app/logger/withlogging"; -import { authOptions } from "./auth/[...nextauth]"; - // `membershipInvite` flow: we know we can add the user to the associated `workspaceId` as // all the checks (decoding the JWT) have been run before. Simply create the membership if // it does not already exist and mark the invitation as consumed. @@ -112,7 +110,7 @@ function canJoinTargetWorkspace( // Verify if there's an existing workspace with the same verified domain that allows auto-joining. // The user will join this workspace if it exists; otherwise, a new workspace is created. async function handleRegularSignupFlow( - session: any, + session: SessionWithUser, user: User, targetWorkspaceId?: string ): Promise<{ @@ -202,7 +200,7 @@ async function handler( req: NextApiRequest, res: NextApiResponse> ): Promise { - const session = await getServerSession(req, res, authOptions); + const session = await getSession(req, res); if (!session) { res.status(401).end(); return; diff --git a/front/pages/index.tsx b/front/pages/index.tsx index 0b797f8a2341..420a7f8bbd06 100644 --- a/front/pages/index.tsx +++ b/front/pages/index.tsx @@ -6,6 +6,7 @@ import { GithubWhiteLogo, GoogleLogo, Hover3D, + LoginIcon, LogoHorizontalColorLogoLayer1, LogoHorizontalColorLogoLayer2, LogoHorizontalWhiteLogo, @@ -14,6 +15,7 @@ import { MoreIcon, NotionLogo, OpenaiWhiteLogo, + RocketIcon, SalesforceLogo, SlackLogo, } from "@dust-tt/sparkle"; @@ -22,7 +24,6 @@ import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import Script from "next/script"; -import { signIn } from "next-auth/react"; import type { ParsedUrlQuery } from "querystring"; import React, { useEffect, useRef, useState } from "react"; import { useCookies } from "react-cookie"; @@ -42,10 +43,6 @@ const defaultFlexClasses = "flex flex-col gap-4"; import { Transition } from "@headlessui/react"; -import { - SignInDropDownButton, - SignUpDropDownButton, -} from "@app/components/Button"; import SimpleSlider from "@app/components/home/carousel"; import Particles from "@app/components/home/particles"; import ScrollingHeader from "@app/components/home/scrollingHeader"; @@ -120,7 +117,7 @@ export default function Home({ } }, []); - function getCallbackUrl(routerQuery: ParsedUrlQuery): string { + function getReturnToUrl(routerQuery: ParsedUrlQuery): string { let callbackUrl = "/api/login"; if (routerQuery.inviteToken) { callbackUrl += `?inviteToken=${routerQuery.inviteToken}`; @@ -156,32 +153,17 @@ export default function Home({

- -
- - signIn("google", { - callbackUrl: getCallbackUrl(router.query), - }) - } - /> -
- { - void signIn("github", { - callbackUrl: getCallbackUrl(router.query), - }); - }} - onClickGoogle={() => - signIn("google", { - callbackUrl: getCallbackUrl(router.query), - }) - } - /> -
+
@@ -246,13 +228,15 @@ export default function Home({ is a competitive edge.
- - signIn("google", { - callbackUrl: getCallbackUrl(router.query), - }) +
@@ -768,13 +752,15 @@ export default function Home({
- - signIn("google", { - callbackUrl: getCallbackUrl(router.query), - }) +
diff --git a/front/pages/w/[wId]/join.tsx b/front/pages/w/[wId]/join.tsx index 6fa6516bf2fe..6f855d57d994 100644 --- a/front/pages/w/[wId]/join.tsx +++ b/front/pages/w/[wId]/join.tsx @@ -1,9 +1,7 @@ -import { GoogleLogo, Logo } from "@dust-tt/sparkle"; +import { Button, LoginIcon, Logo } from "@dust-tt/sparkle"; import type { LightWorkspaceType } from "@dust-tt/types"; import type { InferGetServerSidePropsType } from "next"; -import { signIn } from "next-auth/react"; -import { SignInButton } from "@app/components/Button"; import { A, H1, P, Strong } from "@app/components/home/contentComponents"; import OnboardingLayout from "@app/components/sparkle/OnboardingLayout"; import { @@ -148,14 +146,14 @@ export default function Join({ )}
- { - void signIn("google", { - callbackUrl: signUpCallbackUrl, - }); - }} +
diff --git a/types/src/front/user.ts b/types/src/front/user.ts index 50f0cc81a79a..671c5ecb7767 100644 --- a/types/src/front/user.ts +++ b/types/src/front/user.ts @@ -17,7 +17,7 @@ export type WorkspaceType = LightWorkspaceType & { flags: WhitelistableFeature[]; }; -export type UserProviderType = "github" | "google"; +export type UserProviderType = "github" | "google" | null; export type UserType = { id: ModelId;