Skip to content

Commit 7486d03

Browse files
adiologydevVikhyath Mondreti
authored andcommitted
fix(blocks): workflow handler not working outside gui (#609)
* fix: key to call api internally for workflow block * feat: use jwt for internal auth to avoid a static key * chore: formatter
1 parent 9026c44 commit 7486d03

File tree

6 files changed

+113
-26
lines changed

6 files changed

+113
-26
lines changed

apps/sim/app/api/workflows/[id]/route.ts

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { getSession } from '@/lib/auth'
5+
import { verifyInternalToken } from '@/lib/auth/internal'
56
import { createLogger } from '@/lib/logs/console-logger'
67
import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils'
78
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
@@ -28,14 +29,29 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
2829
const { id: workflowId } = await params
2930

3031
try {
31-
// Get the session
32-
const session = await getSession()
33-
if (!session?.user?.id) {
34-
logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`)
35-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
32+
// Check for internal JWT token for server-side calls
33+
const authHeader = request.headers.get('authorization')
34+
let isInternalCall = false
35+
36+
if (authHeader?.startsWith('Bearer ')) {
37+
const token = authHeader.split(' ')[1]
38+
isInternalCall = await verifyInternalToken(token)
3639
}
3740

38-
const userId = session.user.id
41+
let userId: string | null = null
42+
43+
if (isInternalCall) {
44+
// For internal calls, we'll skip user-specific access checks
45+
logger.info(`[${requestId}] Internal API call for workflow ${workflowId}`)
46+
} else {
47+
// Get the session for regular user calls
48+
const session = await getSession()
49+
if (!session?.user?.id) {
50+
logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`)
51+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
52+
}
53+
userId = session.user.id
54+
}
3955

4056
// Fetch the workflow
4157
const workflowData = await db
@@ -52,26 +68,31 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
5268
// Check if user has access to this workflow
5369
let hasAccess = false
5470

55-
// Case 1: User owns the workflow
56-
if (workflowData.userId === userId) {
71+
if (isInternalCall) {
72+
// Internal calls have full access
5773
hasAccess = true
58-
}
59-
60-
// Case 2: Workflow belongs to a workspace the user has permissions for
61-
if (!hasAccess && workflowData.workspaceId) {
62-
const userPermission = await getUserEntityPermissions(
63-
userId,
64-
'workspace',
65-
workflowData.workspaceId
66-
)
67-
if (userPermission !== null) {
74+
} else {
75+
// Case 1: User owns the workflow
76+
if (workflowData.userId === userId) {
6877
hasAccess = true
6978
}
70-
}
7179

72-
if (!hasAccess) {
73-
logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`)
74-
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
80+
// Case 2: Workflow belongs to a workspace the user has permissions for
81+
if (!hasAccess && workflowData.workspaceId && userId) {
82+
const userPermission = await getUserEntityPermissions(
83+
userId,
84+
'workspace',
85+
workflowData.workspaceId
86+
)
87+
if (userPermission !== null) {
88+
hasAccess = true
89+
}
90+
}
91+
92+
if (!hasAccess) {
93+
logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`)
94+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
95+
}
7596
}
7697

7798
// Try to load from normalized tables first

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { generateInternalToken } from '@/lib/auth/internal'
12
import { createLogger } from '@/lib/logs/console-logger'
3+
import { getBaseUrl } from '@/lib/urls/utils'
24
import type { BlockOutput } from '@/blocks/types'
35
import { Serializer } from '@/serializer'
46
import type { SerializedBlock } from '@/serializer/types'
@@ -125,8 +127,20 @@ export class WorkflowBlockHandler implements BlockHandler {
125127
*/
126128
private async loadChildWorkflow(workflowId: string) {
127129
try {
128-
// Fetch workflow from API
129-
const response = await fetch(`/api/workflows/${workflowId}`)
130+
// Fetch workflow from API with internal authentication header
131+
const headers: Record<string, string> = {
132+
'Content-Type': 'application/json',
133+
}
134+
135+
// Add internal auth header for server-side calls
136+
if (typeof window === 'undefined') {
137+
const token = await generateInternalToken()
138+
headers.Authorization = `Bearer ${token}`
139+
}
140+
141+
const response = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}`, {
142+
headers,
143+
})
130144

131145
if (!response.ok) {
132146
if (response.status === 404) {

apps/sim/lib/auth/internal.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { jwtVerify, SignJWT } from 'jose'
2+
import { env } from '@/lib/env'
3+
4+
// Create a secret key for JWT signing
5+
const getJwtSecret = () => {
6+
const secret = new TextEncoder().encode(env.INTERNAL_API_SECRET)
7+
return secret
8+
}
9+
10+
/**
11+
* Generate an internal JWT token for server-side API calls
12+
* Token expires in 5 minutes to keep it short-lived
13+
*/
14+
export async function generateInternalToken(): Promise<string> {
15+
const secret = getJwtSecret()
16+
17+
const token = await new SignJWT({ type: 'internal' })
18+
.setProtectedHeader({ alg: 'HS256' })
19+
.setIssuedAt()
20+
.setExpirationTime('5m')
21+
.setIssuer('sim-internal')
22+
.setAudience('sim-api')
23+
.sign(secret)
24+
25+
return token
26+
}
27+
28+
/**
29+
* Verify an internal JWT token
30+
* Returns true if valid, false otherwise
31+
*/
32+
export async function verifyInternalToken(token: string): Promise<boolean> {
33+
try {
34+
const secret = getJwtSecret()
35+
36+
const { payload } = await jwtVerify(token, secret, {
37+
issuer: 'sim-internal',
38+
audience: 'sim-api',
39+
})
40+
41+
// Check that it's an internal token
42+
return payload.type === 'internal'
43+
} catch (error) {
44+
// Token verification failed
45+
return false
46+
}
47+
}

apps/sim/lib/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const env = createEnv({
1313
BETTER_AUTH_SECRET: z.string().min(32),
1414
DISABLE_REGISTRATION: z.boolean().optional(),
1515
ENCRYPTION_KEY: z.string().min(32),
16+
INTERNAL_API_SECRET: z.string().min(32),
1617

1718
POSTGRES_URL: z.string().url().optional(),
1819
STRIPE_SECRET_KEY: z.string().min(1).optional(),

apps/sim/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"groq-sdk": "^0.15.0",
8686
"input-otp": "^1.4.2",
8787
"ioredis": "^5.6.0",
88+
"jose": "6.0.11",
8889
"jwt-decode": "^4.0.0",
8990
"lenis": "^1.2.3",
9091
"lucide-react": "^0.479.0",

bun.lock

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)