From 202f127461fc4649f7b6532484568f07bcd35606 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 16:01:18 +0100 Subject: [PATCH 1/9] Use dedicated audience for dust api. Add scopes verification --- extension/app/background.ts | 9 +- extension/app/src/lib/config.ts | 1 + front/lib/api/auth0.ts | 108 +++++++++++++----- front/lib/api/config.ts | 3 + front/lib/api/wrappers.ts | 49 +++++++- front/lib/auth.ts | 3 +- front/pages/api/v1/me.ts | 4 +- .../assistant/conversations/[cId]/index.ts | 4 +- .../w/[wId]/assistant/conversations/index.ts | 4 +- 9 files changed, 144 insertions(+), 41 deletions(-) diff --git a/extension/app/background.ts b/extension/app/background.ts index 21ac20a5d83e..048301a05efe 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,12 +266,13 @@ 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", redirect_uri: redirectUrl, - audience: AUTH0_AUDIENCE, + audience: DUST_API_AUDIENCE, code_challenge_method: "S256", code_challenge: codeChallenge, + prompt: "consent", }; const queryString = new URLSearchParams(options).toString(); 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..88b35f4caca9 100644 --- a/front/lib/api/auth0.ts +++ b/front/lib/api/auth0.ts @@ -1,6 +1,9 @@ +import type { Result } from "@dust-tt/types"; +import { Err, Ok } from "@dust-tt/types"; import { ManagementClient } from "auth0"; 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 +11,47 @@ import logger from "@app/logger/logger"; let auth0ManagemementClient: ManagementClient | null = null; +export interface Auth0JwtPayload extends jwt.JwtPayload { + azp: string; + exp: number; + scope: string; + sub: string; +} + +const METHOD_TO_VERB: Record = { + GET: "read", + POST: "create", + PATCH: "update", + DELETE: "delete", +}; + +export function getRequiredScope( + req: NextApiRequest, + { + resourceName, + }: { + resourceName?: string; + } +) { + if (resourceName && req.method) { + return [`${METHOD_TO_VERB[req.method]}:${resourceName}`]; + } + return undefined; +} + +function isAuth0Payload(payload: jwt.JwtPayload): payload is Auth0JwtPayload { + return ( + "azp" in payload && + typeof payload.azp === "string" && + "exp" in payload && + typeof payload.exp === "number" && + "scope" in payload && + typeof payload.scope === "string" && + "sub" in payload && + typeof payload.sub === "string" + ); +} + export function getAuth0ManagemementClient(): ManagementClient { if (!auth0ManagemementClient) { auth0ManagemementClient = new ManagementClient({ @@ -50,13 +94,16 @@ 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, + requiredScopes?: string[] +): 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) => { + return new Promise((resolve) => { jwt.verify( accessToken, async (header, callback) => { @@ -77,43 +124,46 @@ async function verifyAuth0Token(accessToken: string): Promise { }, (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); + + if (!isAuth0Payload(decoded)) { + logger.error("Invalid token payload."); + return resolve(new Err(Error("Invalid token payload."))); + } + + const now = Math.floor(Date.now() / 1000); + + if (decoded.exp <= now) { + logger.error("Expired token payload."); + return resolve(new Err(Error("Expired token payload."))); + } + + if (requiredScopes) { + const availableScopes = decoded.scope.split(" "); + if ( + requiredScopes.some((scope) => !availableScopes.includes(scope)) + ) { + logger.error("Insufficient scopes."); + return resolve(new Err(Error("Insufficient scopes."))); + } + } + + resolve(new Ok(decoded)); } ); }); } + /** * 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..bee0a1fd8e50 100644 --- a/front/lib/api/wrappers.ts +++ b/front/lib/api/wrappers.ts @@ -5,7 +5,13 @@ import type { } from "@dust-tt/types"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getUserFromAuth0Token } from "@app/lib/api/auth0"; +import { + getRequiredScope, + getUserFromAuth0Token, + METHOD_TO_VERB, + validateScopes, + verifyAuth0Token, +} from "@app/lib/api/auth0"; import { getUserWithWorkspaces } from "@app/lib/api/user"; import { Authenticator, @@ -164,6 +170,7 @@ export function withPublicAPIAuthentication( opts: { isStreaming?: boolean; allowUserOutsideCurrentWorkspace?: U; + resourceName?: string; } = {} ) { const { allowUserOutsideCurrentWorkspace, isStreaming } = opts; @@ -201,8 +208,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) + ); + 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 +344,10 @@ export function withAuth0TokenAuthentication( req: NextApiRequest, res: NextApiResponse>, user: UserTypeWithWorkspaces - ) => Promise | void + ) => Promise | void, + opts: { + resourceName?: string; + } = {} ) { return withLogging( async ( @@ -354,7 +379,23 @@ export function withAuth0TokenAuthentication( }); } - const user = await getUserFromAuth0Token(bearerToken); + const decoded = await verifyAuth0Token( + bearerToken, + getRequiredScope(req, opts) + ); + 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); + 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..bcbf31d27a9a 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, { + resourceName: "user_profile", +}); 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..713591374325 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, { + resourceName: "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..23cd685b7a1f 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, { + resourceName: "conversation", +}); From 0241a9cf5c145711f0155923f96895b46d60bdba Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 16:06:20 +0100 Subject: [PATCH 2/9] remove forced consent screen --- extension/app/background.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extension/app/background.ts b/extension/app/background.ts index 048301a05efe..5b6103cbc633 100644 --- a/extension/app/background.ts +++ b/extension/app/background.ts @@ -272,7 +272,6 @@ const authenticate = async ( audience: DUST_API_AUDIENCE, code_challenge_method: "S256", code_challenge: codeChallenge, - prompt: "consent", }; const queryString = new URLSearchParams(options).toString(); From 663b8b7fdf5297e2f0a4807dec5e372b7a0ecdcb Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 22:40:09 +0100 Subject: [PATCH 3/9] review comments, add scopes --- extension/app/background.ts | 3 +- front/lib/api/auth0.ts | 58 +++++-------------- front/lib/api/wrappers.ts | 8 +-- front/pages/api/v1/me.ts | 2 +- .../w/[wId]/assistant/agent_configurations.ts | 4 +- .../assistant/conversations/[cId]/events.ts | 5 +- .../assistant/conversations/[cId]/index.ts | 2 +- .../[cId]/messages/[mId]/edit.ts | 4 +- .../[cId]/messages/[mId]/events.ts | 5 +- .../[cId]/messages/[mId]/retry.ts | 5 +- .../conversations/[cId]/messages/index.ts | 4 +- .../w/[wId]/assistant/conversations/index.ts | 2 +- 12 files changed, 46 insertions(+), 56 deletions(-) diff --git a/extension/app/background.ts b/extension/app/background.ts index 5b6103cbc633..ef3b3236d032 100644 --- a/extension/app/background.ts +++ b/extension/app/background.ts @@ -267,11 +267,12 @@ const authenticate = async ( client_id: AUTH0_CLIENT_ID, response_type: "code", scope: - "offline_access read:user_profile read:conversation create:conversation", + "offline_access read:user_profile read:conversation create:conversation update:conversation read:agent", redirect_uri: redirectUrl, audience: DUST_API_AUDIENCE, code_challenge_method: "S256", code_challenge: codeChallenge, + prompt: "consent", }; const queryString = new URLSearchParams(options).toString(); diff --git a/front/lib/api/auth0.ts b/front/lib/api/auth0.ts index 88b35f4caca9..9923f66c01ab 100644 --- a/front/lib/api/auth0.ts +++ b/front/lib/api/auth0.ts @@ -1,6 +1,8 @@ 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"; @@ -11,47 +13,25 @@ import logger from "@app/logger/logger"; let auth0ManagemementClient: ManagementClient | null = null; -export interface Auth0JwtPayload extends jwt.JwtPayload { - azp: string; - exp: number; - scope: string; - sub: string; -} +const Auth0JwtPayloadSchema = t.type({ + azp: t.string, + exp: t.number, + scope: t.string, + sub: t.string, +}); -const METHOD_TO_VERB: Record = { - GET: "read", - POST: "create", - PATCH: "update", - DELETE: "delete", -}; +type Auth0JwtPayload = t.TypeOf & jwt.JwtPayload; export function getRequiredScope( req: NextApiRequest, - { - resourceName, - }: { - resourceName?: string; - } + requiredScopes?: Record ) { - if (resourceName && req.method) { - return [`${METHOD_TO_VERB[req.method]}:${resourceName}`]; + if (requiredScopes && req.method && requiredScopes[req.method]) { + return [requiredScopes[req.method]]; } return undefined; } -function isAuth0Payload(payload: jwt.JwtPayload): payload is Auth0JwtPayload { - return ( - "azp" in payload && - typeof payload.azp === "string" && - "exp" in payload && - typeof payload.exp === "number" && - "scope" in payload && - typeof payload.scope === "string" && - "sub" in payload && - typeof payload.sub === "string" - ); -} - export function getAuth0ManagemementClient(): ManagementClient { if (!auth0ManagemementClient) { auth0ManagemementClient = new ManagementClient({ @@ -130,29 +110,23 @@ export async function verifyAuth0Token( return resolve(new Err(Error("No token payload"))); } - if (!isAuth0Payload(decoded)) { + const payloadValidation = Auth0JwtPayloadSchema.decode(decoded); + if (isLeft(payloadValidation)) { logger.error("Invalid token payload."); return resolve(new Err(Error("Invalid token payload."))); } - const now = Math.floor(Date.now() / 1000); - - if (decoded.exp <= now) { - logger.error("Expired token payload."); - return resolve(new Err(Error("Expired token payload."))); - } - if (requiredScopes) { const availableScopes = decoded.scope.split(" "); if ( requiredScopes.some((scope) => !availableScopes.includes(scope)) ) { - logger.error("Insufficient scopes."); + logger.error({ requiredScopes }, "Insufficient scopes."); return resolve(new Err(Error("Insufficient scopes."))); } } - resolve(new Ok(decoded)); + return resolve(new Ok(payloadValidation.right)); } ); }); diff --git a/front/lib/api/wrappers.ts b/front/lib/api/wrappers.ts index bee0a1fd8e50..1bfc84f40e02 100644 --- a/front/lib/api/wrappers.ts +++ b/front/lib/api/wrappers.ts @@ -170,7 +170,7 @@ export function withPublicAPIAuthentication( opts: { isStreaming?: boolean; allowUserOutsideCurrentWorkspace?: U; - resourceName?: string; + requiredScopes?: Record; } = {} ) { const { allowUserOutsideCurrentWorkspace, isStreaming } = opts; @@ -210,7 +210,7 @@ export function withPublicAPIAuthentication( if (authMethod === "access_token") { const decoded = await verifyAuth0Token( token, - getRequiredScope(req, opts) + getRequiredScope(req, opts.requiredScopes) ); if (decoded.isErr()) { return apiError(req, res, { @@ -346,7 +346,7 @@ export function withAuth0TokenAuthentication( user: UserTypeWithWorkspaces ) => Promise | void, opts: { - resourceName?: string; + requiredScopes?: Record; } = {} ) { return withLogging( @@ -381,7 +381,7 @@ export function withAuth0TokenAuthentication( const decoded = await verifyAuth0Token( bearerToken, - getRequiredScope(req, opts) + getRequiredScope(req, opts.requiredScopes) ); if (decoded.isErr()) { return apiError(req, res, { diff --git a/front/pages/api/v1/me.ts b/front/pages/api/v1/me.ts index bcbf31d27a9a..c90cd2fe02c6 100644 --- a/front/pages/api/v1/me.ts +++ b/front/pages/api/v1/me.ts @@ -35,5 +35,5 @@ async function handler( } export default withAuth0TokenAuthentication(handler, { - resourceName: "user_profile", + 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]/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 713591374325..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 @@ -92,5 +92,5 @@ async function handler( } export default withPublicAPIAuthentication(handler, { - resourceName: "conversation", + 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..4d93be2cdf09 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, { + requiredScope: "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 23cd685b7a1f..b565b2bf55ea 100644 --- a/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts +++ b/front/pages/api/v1/w/[wId]/assistant/conversations/index.ts @@ -279,5 +279,5 @@ async function handler( } export default withPublicAPIAuthentication(handler, { - resourceName: "conversation", + requiredScopes: { GET: "read:conversation", POST: "create:conversation" }, }); From bb0dea049d9f2cbff07a01feb32e885921db1285 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 22:41:06 +0100 Subject: [PATCH 4/9] lint --- front/lib/api/wrappers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/front/lib/api/wrappers.ts b/front/lib/api/wrappers.ts index 1bfc84f40e02..4e9253a6a83b 100644 --- a/front/lib/api/wrappers.ts +++ b/front/lib/api/wrappers.ts @@ -8,8 +8,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getRequiredScope, getUserFromAuth0Token, - METHOD_TO_VERB, - validateScopes, verifyAuth0Token, } from "@app/lib/api/auth0"; import { getUserWithWorkspaces } from "@app/lib/api/user"; From 8a9f80fc0ab2a9803967edea50a0e24a48076090 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 22:50:51 +0100 Subject: [PATCH 5/9] lint --- .../v1/w/[wId]/assistant/conversations/[cId]/messages/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d93be2cdf09..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 @@ -139,5 +139,5 @@ async function handler( } export default withPublicAPIAuthentication(handler, { - requiredScope: "update:conversation", + requiredScopes: { POST: "update:conversation" }, }); From 10a9a1af1b35de4027f9846008b97bc5d12720a6 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 23:36:14 +0100 Subject: [PATCH 6/9] add scopes --- extension/app/background.ts | 2 +- .../v1/w/[wId]/assistant/conversations/[cId]/cancel.ts | 1 + .../assistant/conversations/[cId]/content_fragments.ts | 4 +++- front/pages/api/v1/w/[wId]/files/[fileId].ts | 8 +++++++- front/pages/api/v1/w/[wId]/files/index.ts | 4 +++- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/extension/app/background.ts b/extension/app/background.ts index ef3b3236d032..8154d5108ede 100644 --- a/extension/app/background.ts +++ b/extension/app/background.ts @@ -267,7 +267,7 @@ const authenticate = async ( client_id: AUTH0_CLIENT_ID, response_type: "code", scope: - "offline_access read:user_profile read:conversation create:conversation update:conversation read:agent", + "offline_access read:user_profile read:conversation create:conversation update:conversation read:agent read:file create:file delete:file", redirect_uri: redirectUrl, audience: DUST_API_AUDIENCE, code_challenge_method: "S256", 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]/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" }, +}); From dc76e618005ca82d767c582568496abd82d43113 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Mon, 18 Nov 2024 23:41:17 +0100 Subject: [PATCH 7/9] lint --- front/lib/api/auth0.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/front/lib/api/auth0.ts b/front/lib/api/auth0.ts index 9923f66c01ab..a53bc4b08cb9 100644 --- a/front/lib/api/auth0.ts +++ b/front/lib/api/auth0.ts @@ -20,7 +20,8 @@ const Auth0JwtPayloadSchema = t.type({ sub: t.string, }); -type Auth0JwtPayload = t.TypeOf & jwt.JwtPayload; +export type Auth0JwtPayload = t.TypeOf & + jwt.JwtPayload; export function getRequiredScope( req: NextApiRequest, From 738d1ea2d651ae5f622b493279c525fd949fb443 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Tue, 19 Nov 2024 07:52:10 +0100 Subject: [PATCH 8/9] remove forced consent screen --- extension/app/background.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extension/app/background.ts b/extension/app/background.ts index 8154d5108ede..1fc6567ec8fb 100644 --- a/extension/app/background.ts +++ b/extension/app/background.ts @@ -272,7 +272,6 @@ const authenticate = async ( audience: DUST_API_AUDIENCE, code_challenge_method: "S256", code_challenge: codeChallenge, - prompt: "consent", }; const queryString = new URLSearchParams(options).toString(); From 94906612e2a1ee34ba4787dfec1e01d6d91b2618 Mon Sep 17 00:00:00 2001 From: Thomas Draier Date: Tue, 19 Nov 2024 10:15:16 +0100 Subject: [PATCH 9/9] types improvements, handle legacy audience --- front/lib/api/auth0.ts | 58 +++++++++++++++++++++++++++++++-------- front/lib/api/wrappers.ts | 9 +++--- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/front/lib/api/auth0.ts b/front/lib/api/auth0.ts index a53bc4b08cb9..4530cd78ea27 100644 --- a/front/lib/api/auth0.ts +++ b/front/lib/api/auth0.ts @@ -13,7 +13,30 @@ import logger from "@app/logger/logger"; let auth0ManagemementClient: ManagementClient | null = null; -const Auth0JwtPayloadSchema = t.type({ +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, @@ -25,10 +48,17 @@ export type Auth0JwtPayload = t.TypeOf & export function getRequiredScope( req: NextApiRequest, - requiredScopes?: Record + requiredScopes?: Partial> ) { - if (requiredScopes && req.method && requiredScopes[req.method]) { - return [requiredScopes[req.method]]; + const method = req.method; + + if ( + method && + isSupportedMethod(method) && + requiredScopes && + requiredScopes[method] + ) { + return requiredScopes[method]; } return undefined; } @@ -77,13 +107,18 @@ async function getSigningKey(jwksUri: string, kid: string): Promise { */ export async function verifyAuth0Token( accessToken: string, - requiredScopes?: string[] + requiredScope?: ScopeType ): Promise> { const auth0Domain = config.getAuth0TenantUrl(); const audience = config.getDustApiAudience(); const verify = `https://${auth0Domain}/.well-known/jwks.json`; const issuer = `https://${auth0Domain}/`; + // 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, @@ -100,7 +135,7 @@ export async function verifyAuth0Token( }, { algorithms: ["RS256"], - audience: audience, + audience: useLegacy ? legacyAudience : audience, issuer: issuer, }, (err, decoded) => { @@ -117,12 +152,13 @@ export async function verifyAuth0Token( return resolve(new Err(Error("Invalid token payload."))); } - if (requiredScopes) { + if (requiredScope && !useLegacy) { const availableScopes = decoded.scope.split(" "); - if ( - requiredScopes.some((scope) => !availableScopes.includes(scope)) - ) { - logger.error({ requiredScopes }, "Insufficient scopes."); + if (!availableScopes.includes(requiredScope)) { + logger.error( + { requiredScopes: requiredScope }, + "Insufficient scopes." + ); return resolve(new Err(Error("Insufficient scopes."))); } } diff --git a/front/lib/api/wrappers.ts b/front/lib/api/wrappers.ts index 4e9253a6a83b..e28e96ce9a1d 100644 --- a/front/lib/api/wrappers.ts +++ b/front/lib/api/wrappers.ts @@ -5,6 +5,7 @@ import type { } from "@dust-tt/types"; import type { NextApiRequest, NextApiResponse } from "next"; +import type { MethodType, ScopeType } from "@app/lib/api/auth0"; import { getRequiredScope, getUserFromAuth0Token, @@ -51,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.", }, }); } @@ -168,7 +169,7 @@ export function withPublicAPIAuthentication( opts: { isStreaming?: boolean; allowUserOutsideCurrentWorkspace?: U; - requiredScopes?: Record; + requiredScopes?: Partial>; } = {} ) { const { allowUserOutsideCurrentWorkspace, isStreaming } = opts; @@ -344,7 +345,7 @@ export function withAuth0TokenAuthentication( user: UserTypeWithWorkspaces ) => Promise | void, opts: { - requiredScopes?: Record; + requiredScopes?: Partial>; } = {} ) { return withLogging( @@ -393,7 +394,7 @@ export function withAuth0TokenAuthentication( } 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,