From 1c10912d3e94fbd5ad7bf556d7e9879c106ad350 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 2 Jan 2025 13:32:18 +0530 Subject: [PATCH 1/2] Enhance OAuth flow role validation --- apps/web/app/api/oauth/authorize/route.ts | 15 +++++++++ .../(auth)/oauth/authorize/authorize-form.tsx | 33 ++++++++++++++++++- .../oauth/authorize/scopes-requested.tsx | 18 +--------- apps/web/lib/api/tokens/scopes.ts | 22 +++++++++++++ 4 files changed, 70 insertions(+), 18 deletions(-) diff --git a/apps/web/app/api/oauth/authorize/route.ts b/apps/web/app/api/oauth/authorize/route.ts index 9d56babf5f..88f3d4a4d7 100644 --- a/apps/web/app/api/oauth/authorize/route.ts +++ b/apps/web/app/api/oauth/authorize/route.ts @@ -1,6 +1,7 @@ import { DubApiError } from "@/lib/api/errors"; import { OAUTH_CONFIG } from "@/lib/api/oauth/constants"; import { createToken } from "@/lib/api/oauth/utils"; +import { consolidateScopes, getScopesForRole } from "@/lib/api/tokens/scopes"; import { parseRequestBody } from "@/lib/api/utils"; import { withWorkspace } from "@/lib/auth"; import { authorizeRequestSchema } from "@/lib/zod/schemas/oauth"; @@ -18,6 +19,20 @@ export const POST = withWorkspace(async ({ session, req, workspace }) => { code_challenge_method: codeChallengeMethod, } = authorizeRequestSchema.parse(await parseRequestBody(req)); + // Check if the user has the required scopes for the workspace selected + const userRole = workspace.users[0].role; + const scopesForRole = getScopesForRole(userRole); + const scopesMissing = consolidateScopes(scope).filter( + (scope) => !scopesForRole.includes(scope) && scope !== "user.read", + ); + + if (scopesMissing.length > 0) { + throw new DubApiError({ + code: "bad_request", + message: "You don't have the permission to install this integration.", + }); + } + const app = await prisma.oAuthApp.findUniqueOrThrow({ where: { clientId, diff --git a/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx b/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx index cd0d5a7e82..9bcede385c 100644 --- a/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx +++ b/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx @@ -1,5 +1,6 @@ "use client"; +import { consolidateScopes, getScopesForRole } from "@/lib/api/tokens/scopes"; import useWorkspaces from "@/lib/swr/use-workspaces"; import z from "@/lib/zod"; import { authorizeRequestSchema } from "@/lib/zod/schemas/oauth"; @@ -32,6 +33,9 @@ export const AuthorizeForm = ({ const [selectedWorkspace, setSelectedWorkspace] = useState(null); + // missing scopes for the user's role on the workspace selected + const [missingScopes, setMissingScopes] = useState([]); + const selectOptions = useMemo(() => { return workspaces ? workspaces.map((workspace) => ({ @@ -50,6 +54,29 @@ export const AuthorizeForm = ({ ); }, [selectOptions, session]); + // Check if the user has the required scopes for the workspace selected + useEffect(() => { + if (!selectedWorkspace) { + return; + } + + const workspace = workspaces?.find( + (workspace) => workspace.slug === selectedWorkspace.id, + ); + + if (!workspace) { + return; + } + + const userRole = workspace.users[0].role; + const scopesForRole = getScopesForRole(userRole); + const scopesMissing = consolidateScopes(scope).filter( + (scope) => !scopesForRole.includes(scope) && scope !== "user.read", + ); + + setMissingScopes(scopesMissing); + }, [selectedWorkspace]); + // Decline the request const onDecline = () => { const searchParams = new URLSearchParams({ @@ -148,7 +175,11 @@ export const AuthorizeForm = ({ loading={submitting} disabled={!selectedWorkspace} disabledTooltip={ - !selectedWorkspace ? "Please select a workspace to continue" : "" + !selectedWorkspace + ? "Please select a workspace to continue" + : missingScopes.length > 0 + ? "You don't have the permission to install this integration" + : "" } /> diff --git a/apps/web/app/app.dub.co/(auth)/oauth/authorize/scopes-requested.tsx b/apps/web/app/app.dub.co/(auth)/oauth/authorize/scopes-requested.tsx index e9d61ee9bf..c0acf325f1 100644 --- a/apps/web/app/app.dub.co/(auth)/oauth/authorize/scopes-requested.tsx +++ b/apps/web/app/app.dub.co/(auth)/oauth/authorize/scopes-requested.tsx @@ -1,6 +1,7 @@ "use client"; import { OAUTH_SCOPE_DESCRIPTIONS } from "@/lib/api/oauth/constants"; +import { consolidateScopes } from "@/lib/api/tokens/scopes"; import { Check } from "lucide-react"; interface ScopesProps { @@ -48,20 +49,3 @@ export const ScopesRequested = ({ scopes }: ScopesProps) => { ); }; - -// Consolidate scopes to avoid duplication and show only the most permissive scope -const consolidateScopes = (scopes: string[]) => { - const consolidated = new Set(); - - scopes.forEach((scope) => { - const [resource, action] = scope.split("."); - - if (action === "write") { - consolidated.add(`${resource}.write`); - } else if (action === "read" && !consolidated.has(`${resource}.write`)) { - consolidated.add(`${resource}.read`); - } - }); - - return Array.from(consolidated) as string[]; -}; diff --git a/apps/web/lib/api/tokens/scopes.ts b/apps/web/lib/api/tokens/scopes.ts index ffc5c2c1c0..c3e3606b38 100644 --- a/apps/web/lib/api/tokens/scopes.ts +++ b/apps/web/lib/api/tokens/scopes.ts @@ -246,3 +246,25 @@ export const validateScopesForRole = (scopes: Scope[], role: Role) => { return !(invalidScopes.length > 0); }; + +// Get the scopes for a role +export const getScopesForRole = (role: Role) => { + return ROLE_SCOPES_MAP[role]; +}; + +// Consolidate scopes to avoid duplication and show only the most permissive scope +export const consolidateScopes = (scopes: string[]) => { + const consolidated = new Set(); + + scopes.forEach((scope) => { + const [resource, action] = scope.split("."); + + if (action === "write") { + consolidated.add(`${resource}.write`); + } else if (action === "read" && !consolidated.has(`${resource}.write`)) { + consolidated.add(`${resource}.read`); + } + }); + + return Array.from(consolidated) as string[]; +}; From 4e08574628e95f5984a9a39f2757b408c07d08fa Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 2 Jan 2025 07:44:24 -0800 Subject: [PATCH 2/2] Update authorize-form.tsx --- .../app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx b/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx index 9bcede385c..7a57f6d22e 100644 --- a/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx +++ b/apps/web/app/app.dub.co/(auth)/oauth/authorize/authorize-form.tsx @@ -179,7 +179,7 @@ export const AuthorizeForm = ({ ? "Please select a workspace to continue" : missingScopes.length > 0 ? "You don't have the permission to install this integration" - : "" + : undefined } />