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),
- })
- }
- />
-
+