diff --git a/extension/app/background.ts b/extension/app/background.ts index 21ac20a5d83e..1fc6567ec8fb 100644 --- a/extension/app/background.ts +++ b/extension/app/background.ts @@ -2,9 +2,9 @@ import type { PendingUpdate } from "@extension/lib/storage"; import { savePendingUpdate } from "@extension/lib/storage"; import { - AUTH0_AUDIENCE, AUTH0_CLIENT_DOMAIN, AUTH0_CLIENT_ID, + DUST_API_AUDIENCE, } from "./src/lib/config"; import { extractPage } from "./src/lib/extraction"; import type { @@ -266,10 +266,10 @@ const authenticate = async ( const options = { client_id: AUTH0_CLIENT_ID, response_type: "code", - // "offline_access" to receive refresh tokens to maintain user sessions without re-prompting for authentication. - scope: "openid offline_access", + scope: + "offline_access read:user_profile read:conversation create:conversation update:conversation read:agent read:file create:file delete:file", redirect_uri: redirectUrl, - audience: AUTH0_AUDIENCE, + audience: DUST_API_AUDIENCE, code_challenge_method: "S256", code_challenge: codeChallenge, }; diff --git a/extension/app/src/lib/config.ts b/extension/app/src/lib/config.ts index bcbf702dbc2c..d60a68d0ed56 100644 --- a/extension/app/src/lib/config.ts +++ b/extension/app/src/lib/config.ts @@ -3,3 +3,4 @@ export const AUTH0_CLIENT_DOMAIN = process.env.AUTH0_CLIENT_DOMAIN ?? ""; export const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID ?? ""; export const AUTH0_AUDIENCE = `https://${AUTH0_CLIENT_DOMAIN}/api/v2/`; export const AUTH0_PROFILE_ROUTE = `https://${AUTH0_CLIENT_DOMAIN}/userinfo`; +export const DUST_API_AUDIENCE = process.env.DUST_API_AUDIENCE ?? ""; diff --git a/front/lib/api/auth0.ts b/front/lib/api/auth0.ts index 2ad668ff0da1..4530cd78ea27 100644 --- a/front/lib/api/auth0.ts +++ b/front/lib/api/auth0.ts @@ -1,6 +1,11 @@ +import type { Result } from "@dust-tt/types"; +import { Err, Ok } from "@dust-tt/types"; import { ManagementClient } from "auth0"; +import { isLeft } from "fp-ts/lib/Either"; +import * as t from "io-ts"; import jwt from "jsonwebtoken"; import jwksClient from "jwks-rsa"; +import type { NextApiRequest } from "next"; import config from "@app/lib/api/config"; import { UserResource } from "@app/lib/resources/user_resource"; @@ -8,6 +13,56 @@ import logger from "@app/logger/logger"; let auth0ManagemementClient: ManagementClient | null = null; +export const SUPPORTED_METHODS = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", +] as const; +export type MethodType = (typeof SUPPORTED_METHODS)[number]; + +const isSupportedMethod = (method: string): method is MethodType => + SUPPORTED_METHODS.includes(method as MethodType); + +export type ScopeType = + | "read:user_profile" + | "read:conversation" + | "update:conversation" + | "create:conversation" + | "read:file" + | "update:file" + | "create:file" + | "delete:file" + | "read:agent"; + +export const Auth0JwtPayloadSchema = t.type({ + azp: t.string, + exp: t.number, + scope: t.string, + sub: t.string, +}); + +export type Auth0JwtPayload = t.TypeOf & + jwt.JwtPayload; + +export function getRequiredScope( + req: NextApiRequest, + requiredScopes?: Partial> +) { + const method = req.method; + + if ( + method && + isSupportedMethod(method) && + requiredScopes && + requiredScopes[method] + ) { + return requiredScopes[method]; + } + return undefined; +} + export function getAuth0ManagemementClient(): ManagementClient { if (!auth0ManagemementClient) { auth0ManagemementClient = new ManagementClient({ @@ -50,13 +105,21 @@ async function getSigningKey(jwksUri: string, kid: string): Promise { * Verify an Auth0 token. * Not meant to be exported, use `getUserFromAuth0Token` instead. */ -async function verifyAuth0Token(accessToken: string): Promise { +export async function verifyAuth0Token( + accessToken: string, + requiredScope?: ScopeType +): Promise> { const auth0Domain = config.getAuth0TenantUrl(); - const audience = `https://${auth0Domain}/api/v2/`; + const audience = config.getDustApiAudience(); const verify = `https://${auth0Domain}/.well-known/jwks.json`; const issuer = `https://${auth0Domain}/`; - return new Promise((resolve, reject) => { + // TODO(thomas): Remove this when all clients are updated. + const legacyAudience = `https://${auth0Domain}/api/v2/`; + const decoded = jwt.decode(accessToken, { json: true }); + const useLegacy = decoded && decoded.aud === legacyAudience; + + return new Promise((resolve) => { jwt.verify( accessToken, async (header, callback) => { @@ -72,48 +135,46 @@ async function verifyAuth0Token(accessToken: string): Promise { }, { algorithms: ["RS256"], - audience: audience, + audience: useLegacy ? legacyAudience : audience, issuer: issuer, }, (err, decoded) => { if (err) { - reject(err); - return; + return resolve(new Err(err)); } if (!decoded || typeof decoded !== "object") { - reject(new Error("No token payload")); - return; + return resolve(new Err(Error("No token payload"))); } - resolve(decoded); + + const payloadValidation = Auth0JwtPayloadSchema.decode(decoded); + if (isLeft(payloadValidation)) { + logger.error("Invalid token payload."); + return resolve(new Err(Error("Invalid token payload."))); + } + + if (requiredScope && !useLegacy) { + const availableScopes = decoded.scope.split(" "); + if (!availableScopes.includes(requiredScope)) { + logger.error( + { requiredScopes: requiredScope }, + "Insufficient scopes." + ); + return resolve(new Err(Error("Insufficient scopes."))); + } + } + + return resolve(new Ok(payloadValidation.right)); } ); }); } + /** * Get a user resource from an Auth0 token. * We return the user from its Auth0 sub, only if the token is not expired. */ export async function getUserFromAuth0Token( - accessToken: string + accessToken: Auth0JwtPayload ): Promise { - let decoded: jwt.JwtPayload; - try { - decoded = await verifyAuth0Token(accessToken); - } catch (error) { - logger.error({ error }, "Error verifying Auth0 token"); - return null; - } - - const now = Math.floor(Date.now() / 1000); - - if ( - typeof decoded.sub !== "string" || - typeof decoded.exp !== "number" || - decoded.exp <= now - ) { - logger.error("Invalid or expired token payload."); - return null; - } - - return UserResource.fetchByAuth0Sub(decoded.sub); + return UserResource.fetchByAuth0Sub(accessToken.sub); } diff --git a/front/lib/api/config.ts b/front/lib/api/config.ts index ae8e98e8e53a..a4fd1f564f4a 100644 --- a/front/lib/api/config.ts +++ b/front/lib/api/config.ts @@ -11,6 +11,9 @@ const config = { getAuth0TenantUrl: (): string => { return EnvironmentConfig.getEnvVariable("AUTH0_TENANT_DOMAIN_URL"); }, + getDustApiAudience: (): string => { + return EnvironmentConfig.getEnvVariable("DUST_API_AUDIENCE"); + }, getAuth0M2MClientId: (): string => { return EnvironmentConfig.getEnvVariable("AUTH0_M2M_CLIENT_ID"); }, diff --git a/front/lib/api/wrappers.ts b/front/lib/api/wrappers.ts index 3ff2ea4b47b4..e28e96ce9a1d 100644 --- a/front/lib/api/wrappers.ts +++ b/front/lib/api/wrappers.ts @@ -5,7 +5,12 @@ import type { } from "@dust-tt/types"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getUserFromAuth0Token } from "@app/lib/api/auth0"; +import type { MethodType, ScopeType } from "@app/lib/api/auth0"; +import { + getRequiredScope, + getUserFromAuth0Token, + verifyAuth0Token, +} from "@app/lib/api/auth0"; import { getUserWithWorkspaces } from "@app/lib/api/user"; import { Authenticator, @@ -47,7 +52,7 @@ export function withSessionAuthentication( api_error: { type: "not_authenticated", message: - "The user does not have an active session or is not authenticated", + "The user does not have an active session or is not authenticated.", }, }); } @@ -164,6 +169,7 @@ export function withPublicAPIAuthentication( opts: { isStreaming?: boolean; allowUserOutsideCurrentWorkspace?: U; + requiredScopes?: Partial>; } = {} ) { const { allowUserOutsideCurrentWorkspace, isStreaming } = opts; @@ -201,8 +207,23 @@ export function withPublicAPIAuthentication( // Authentification with Auth0 token. // Straightforward since the token is attached to the user. if (authMethod === "access_token") { - const auth = await Authenticator.fromAuth0Token({ + const decoded = await verifyAuth0Token( token, + getRequiredScope(req, opts.requiredScopes) + ); + if (decoded.isErr()) { + return apiError(req, res, { + status_code: 401, + api_error: { + type: "not_authenticated", + message: + "The request does not have valid authentication credentials.", + }, + }); + } + + const auth = await Authenticator.fromAuth0Token({ + token: decoded.value, wId, }); if (auth.user() === null) { @@ -322,7 +343,10 @@ export function withAuth0TokenAuthentication( req: NextApiRequest, res: NextApiResponse>, user: UserTypeWithWorkspaces - ) => Promise | void + ) => Promise | void, + opts: { + requiredScopes?: Partial>; + } = {} ) { return withLogging( async ( @@ -354,7 +378,23 @@ export function withAuth0TokenAuthentication( }); } - const user = await getUserFromAuth0Token(bearerToken); + const decoded = await verifyAuth0Token( + bearerToken, + getRequiredScope(req, opts.requiredScopes) + ); + if (decoded.isErr()) { + return apiError(req, res, { + status_code: 401, + api_error: { + type: "not_authenticated", + message: + "The request does not have valid authentication credentials.", + }, + }); + } + + const user = await getUserFromAuth0Token(decoded.value); + // TODO(thomas): user not found : means the user is not registered, display a message to the user and redirects to site if (!user) { return apiError(req, res, { status_code: 401, diff --git a/front/lib/auth.ts b/front/lib/auth.ts index 4fb4a962d8de..24ae1bde2d6b 100644 --- a/front/lib/auth.ts +++ b/front/lib/auth.ts @@ -29,6 +29,7 @@ import type { NextApiResponse, } from "next"; +import type { Auth0JwtPayload } from "@app/lib/api/auth0"; import { getUserFromAuth0Token } from "@app/lib/api/auth0"; import config from "@app/lib/api/config"; import type { SessionWithUser } from "@app/lib/iam/provider"; @@ -299,7 +300,7 @@ export class Authenticator { token, wId, }: { - token: string; + token: Auth0JwtPayload; wId: string; }): Promise { const user = await getUserFromAuth0Token(token); diff --git a/front/pages/api/v1/me.ts b/front/pages/api/v1/me.ts index d3a16749b797..c90cd2fe02c6 100644 --- a/front/pages/api/v1/me.ts +++ b/front/pages/api/v1/me.ts @@ -34,4 +34,6 @@ async function handler( } } -export default withAuth0TokenAuthentication(handler); +export default withAuth0TokenAuthentication(handler, { + requiredScopes: { GET: "read:user_profile" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/agent_configurations.ts b/front/pages/api/v1/w/[wId]/assistant/agent_configurations.ts index 3137e664918c..9461f6c1234d 100644 --- a/front/pages/api/v1/w/[wId]/assistant/agent_configurations.ts +++ b/front/pages/api/v1/w/[wId]/assistant/agent_configurations.ts @@ -74,4 +74,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { GET: "read:agent" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/cancel.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/cancel.ts index 0398c5d61b4c..4aa73ab16c76 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/cancel.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/cancel.ts @@ -116,4 +116,5 @@ async function handler( export default withPublicAPIAuthentication(handler, { isStreaming: true, + requiredScopes: { POST: "update:conversation" }, }); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts index 7ed311b361ee..6573e25015b2 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/content_fragments.ts @@ -142,4 +142,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { POST: "update:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/events.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/events.ts index 60c95f1c1f05..813059697107 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/events.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/events.ts @@ -108,4 +108,7 @@ async function handler( } } -export default withPublicAPIAuthentication(handler, { isStreaming: true }); +export default withPublicAPIAuthentication(handler, { + isStreaming: true, + requiredScopes: { GET: "read:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/index.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/index.ts index 1827b1d22990..be5c1fb16ab4 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/index.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/index.ts @@ -91,4 +91,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { GET: "read:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/edit.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/edit.ts index f447b59806af..df7988b95986 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/edit.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/edit.ts @@ -172,4 +172,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { POST: "update:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/events.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/events.ts index 95ea3e7571ae..74864daa5888 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/events.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/events.ts @@ -175,4 +175,7 @@ async function handler( } } -export default withPublicAPIAuthentication(handler, { isStreaming: true }); +export default withPublicAPIAuthentication(handler, { + isStreaming: true, + requiredScopes: { GET: "read:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/retry.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/retry.ts index a953c7e9db53..e30cb11d3307 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/retry.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/[mId]/retry.ts @@ -112,4 +112,7 @@ async function handler( } } -export default withPublicAPIAuthentication(handler, { isStreaming: true }); +export default withPublicAPIAuthentication(handler, { + isStreaming: true, + requiredScopes: { POST: "update:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/index.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/index.ts index cbd8a543a932..750291e0a360 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/index.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/[cId]/messages/index.ts @@ -138,4 +138,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { POST: "update:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts b/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts index 3284f4bb1fee..b565b2bf55ea 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts @@ -278,4 +278,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { GET: "read:conversation", POST: "create:conversation" }, +}); diff --git a/front/pages/api/v1/w/[wId]/files/[fileId].ts b/front/pages/api/v1/w/[wId]/files/[fileId].ts index 113693b6fa5e..bea22f6402f3 100644 --- a/front/pages/api/v1/w/[wId]/files/[fileId].ts +++ b/front/pages/api/v1/w/[wId]/files/[fileId].ts @@ -123,4 +123,10 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { + GET: "read:file", + POST: "create:file", + DELETE: "delete:file", + }, +}); diff --git a/front/pages/api/v1/w/[wId]/files/index.ts b/front/pages/api/v1/w/[wId]/files/index.ts index c31215ac006f..ee76878fbb39 100644 --- a/front/pages/api/v1/w/[wId]/files/index.ts +++ b/front/pages/api/v1/w/[wId]/files/index.ts @@ -167,4 +167,6 @@ async function handler( } } -export default withPublicAPIAuthentication(handler); +export default withPublicAPIAuthentication(handler, { + requiredScopes: { POST: "create:file" }, +});