diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index fd0286faaf..fb7cc0c824 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { verifyInternalToken } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console-logger' import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' @@ -28,14 +29,29 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const { id: workflowId } = await params try { - // Get the session - const session = await getSession() - if (!session?.user?.id) { - logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`) - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + // Check for internal JWT token for server-side calls + const authHeader = request.headers.get('authorization') + let isInternalCall = false + + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.split(' ')[1] + isInternalCall = await verifyInternalToken(token) } - const userId = session.user.id + let userId: string | null = null + + if (isInternalCall) { + // For internal calls, we'll skip user-specific access checks + logger.info(`[${requestId}] Internal API call for workflow ${workflowId}`) + } else { + // Get the session for regular user calls + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + userId = session.user.id + } // Fetch the workflow const workflowData = await db @@ -52,26 +68,31 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ // Check if user has access to this workflow let hasAccess = false - // Case 1: User owns the workflow - if (workflowData.userId === userId) { + if (isInternalCall) { + // Internal calls have full access hasAccess = true - } - - // Case 2: Workflow belongs to a workspace the user has permissions for - if (!hasAccess && workflowData.workspaceId) { - const userPermission = await getUserEntityPermissions( - userId, - 'workspace', - workflowData.workspaceId - ) - if (userPermission !== null) { + } else { + // Case 1: User owns the workflow + if (workflowData.userId === userId) { hasAccess = true } - } - if (!hasAccess) { - logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`) - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + // Case 2: Workflow belongs to a workspace the user has permissions for + if (!hasAccess && workflowData.workspaceId && userId) { + const userPermission = await getUserEntityPermissions( + userId, + 'workspace', + workflowData.workspaceId + ) + if (userPermission !== null) { + hasAccess = true + } + } + + if (!hasAccess) { + logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } } // Try to load from normalized tables first diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 3c3f024145..8aa2625736 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -1,4 +1,6 @@ +import { generateInternalToken } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console-logger' +import { getBaseUrl } from '@/lib/urls/utils' import type { BlockOutput } from '@/blocks/types' import { Serializer } from '@/serializer' import type { SerializedBlock } from '@/serializer/types' @@ -125,8 +127,20 @@ export class WorkflowBlockHandler implements BlockHandler { */ private async loadChildWorkflow(workflowId: string) { try { - // Fetch workflow from API - const response = await fetch(`/api/workflows/${workflowId}`) + // Fetch workflow from API with internal authentication header + const headers: Record = { + 'Content-Type': 'application/json', + } + + // Add internal auth header for server-side calls + if (typeof window === 'undefined') { + const token = await generateInternalToken() + headers.Authorization = `Bearer ${token}` + } + + const response = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}`, { + headers, + }) if (!response.ok) { if (response.status === 404) { diff --git a/apps/sim/lib/auth/internal.ts b/apps/sim/lib/auth/internal.ts new file mode 100644 index 0000000000..1f70b1af45 --- /dev/null +++ b/apps/sim/lib/auth/internal.ts @@ -0,0 +1,47 @@ +import { jwtVerify, SignJWT } from 'jose' +import { env } from '@/lib/env' + +// Create a secret key for JWT signing +const getJwtSecret = () => { + const secret = new TextEncoder().encode(env.INTERNAL_API_SECRET) + return secret +} + +/** + * Generate an internal JWT token for server-side API calls + * Token expires in 5 minutes to keep it short-lived + */ +export async function generateInternalToken(): Promise { + const secret = getJwtSecret() + + const token = await new SignJWT({ type: 'internal' }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('5m') + .setIssuer('sim-internal') + .setAudience('sim-api') + .sign(secret) + + return token +} + +/** + * Verify an internal JWT token + * Returns true if valid, false otherwise + */ +export async function verifyInternalToken(token: string): Promise { + try { + const secret = getJwtSecret() + + const { payload } = await jwtVerify(token, secret, { + issuer: 'sim-internal', + audience: 'sim-api', + }) + + // Check that it's an internal token + return payload.type === 'internal' + } catch (error) { + // Token verification failed + return false + } +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 45287305e9..79247c4467 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -13,6 +13,7 @@ export const env = createEnv({ BETTER_AUTH_SECRET: z.string().min(32), DISABLE_REGISTRATION: z.boolean().optional(), ENCRYPTION_KEY: z.string().min(32), + INTERNAL_API_SECRET: z.string().min(32), POSTGRES_URL: z.string().url().optional(), STRIPE_SECRET_KEY: z.string().min(1).optional(), diff --git a/apps/sim/package.json b/apps/sim/package.json index 934a79e789..9e784b1845 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -85,6 +85,7 @@ "groq-sdk": "^0.15.0", "input-otp": "^1.4.2", "ioredis": "^5.6.0", + "jose": "6.0.11", "jwt-decode": "^4.0.0", "lenis": "^1.2.3", "lucide-react": "^0.479.0", diff --git a/bun.lock b/bun.lock index 2caa45c2fb..ac1f838349 100644 --- a/bun.lock +++ b/bun.lock @@ -115,6 +115,7 @@ "groq-sdk": "^0.15.0", "input-otp": "^1.4.2", "ioredis": "^5.6.0", + "jose": "6.0.11", "jwt-decode": "^4.0.0", "lenis": "^1.2.3", "lucide-react": "^0.479.0", @@ -173,7 +174,7 @@ }, "packages/cli": { "name": "simstudio", - "version": "0.1.18", + "version": "0.1.19", "bin": { "simstudio": "dist/index.js", }, @@ -2131,7 +2132,7 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -3405,6 +3406,8 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "better-auth/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],