mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(security): restrict API key access on internal-only routes (#2964)
* fix(security): restrict API key access on internal-only routes * test(security): update function execute tests for checkInternalAuth * updated agent handler * move session check higher in checkSessionOrInternalAuth * extracted duplicate code into helper for resolving user from jwt
This commit is contained in:
@@ -16,6 +16,168 @@ export interface AuthResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves userId from a verified internal JWT token.
|
||||
* Extracts workflowId/userId from URL params or POST body, then looks up userId if needed.
|
||||
*/
|
||||
async function resolveUserFromJwt(
|
||||
request: NextRequest,
|
||||
verificationUserId: string | null,
|
||||
options: { requireWorkflowId?: boolean }
|
||||
): Promise<AuthResult> {
|
||||
let workflowId: string | null = null
|
||||
let userId: string | null = verificationUserId
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
workflowId = searchParams.get('workflowId')
|
||||
if (!userId) {
|
||||
userId = searchParams.get('userId')
|
||||
}
|
||||
|
||||
if (!workflowId && !userId && request.method === 'POST') {
|
||||
try {
|
||||
const clonedRequest = request.clone()
|
||||
const bodyText = await clonedRequest.text()
|
||||
if (bodyText) {
|
||||
const body = JSON.parse(bodyText)
|
||||
workflowId = body.workflowId || body._context?.workflowId
|
||||
userId = userId || body.userId || body._context?.userId
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
return { success: true, userId, authType: 'internal_jwt' }
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
const [workflowData] = await db
|
||||
.select({ userId: workflow.userId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowData) {
|
||||
return { success: false, error: 'Workflow not found' }
|
||||
}
|
||||
|
||||
return { success: true, userId: workflowData.userId, authType: 'internal_jwt' }
|
||||
}
|
||||
|
||||
if (options.requireWorkflowId !== false) {
|
||||
return { success: false, error: 'workflowId or userId required for internal JWT calls' }
|
||||
}
|
||||
|
||||
return { success: true, authType: 'internal_jwt' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for internal JWT authentication only.
|
||||
* Use this for routes that should ONLY be accessible by the executor (server-to-server).
|
||||
* Rejects session and API key authentication.
|
||||
*
|
||||
* @param request - The incoming request
|
||||
* @param options - Optional configuration
|
||||
* @param options.requireWorkflowId - Whether workflowId/userId is required (default: true)
|
||||
*/
|
||||
export async function checkInternalAuth(
|
||||
request: NextRequest,
|
||||
options: { requireWorkflowId?: boolean } = {}
|
||||
): Promise<AuthResult> {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
|
||||
const apiKeyHeader = request.headers.get('x-api-key')
|
||||
if (apiKeyHeader) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'API key access not allowed for this endpoint. Use workflow execution instead.',
|
||||
}
|
||||
}
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Internal authentication required',
|
||||
}
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1]
|
||||
const verification = await verifyInternalToken(token)
|
||||
|
||||
if (!verification.valid) {
|
||||
return { success: false, error: 'Invalid internal token' }
|
||||
}
|
||||
|
||||
return resolveUserFromJwt(request, verification.userId || null, options)
|
||||
} catch (error) {
|
||||
logger.error('Error in internal authentication:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: 'Authentication error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for session or internal JWT authentication.
|
||||
* Use this for routes that should be accessible by the UI and executor,
|
||||
* but NOT by external API keys.
|
||||
*
|
||||
* @param request - The incoming request
|
||||
* @param options - Optional configuration
|
||||
* @param options.requireWorkflowId - Whether workflowId/userId is required for JWT (default: true)
|
||||
*/
|
||||
export async function checkSessionOrInternalAuth(
|
||||
request: NextRequest,
|
||||
options: { requireWorkflowId?: boolean } = {}
|
||||
): Promise<AuthResult> {
|
||||
try {
|
||||
// 1. Reject API keys first
|
||||
const apiKeyHeader = request.headers.get('x-api-key')
|
||||
if (apiKeyHeader) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'API key access not allowed for this endpoint',
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check for internal JWT token
|
||||
const authHeader = request.headers.get('authorization')
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const token = authHeader.split(' ')[1]
|
||||
const verification = await verifyInternalToken(token)
|
||||
|
||||
if (verification.valid) {
|
||||
return resolveUserFromJwt(request, verification.userId || null, options)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Try session auth (for web UI)
|
||||
const session = await getSession()
|
||||
if (session?.user?.id) {
|
||||
return {
|
||||
success: true,
|
||||
userId: session.user.id,
|
||||
authType: 'session',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Authentication required - provide session or internal JWT',
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in session/internal authentication:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: 'Authentication error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for authentication using any of the 3 supported methods:
|
||||
* 1. Session authentication (cookies)
|
||||
@@ -36,70 +198,7 @@ export async function checkHybridAuth(
|
||||
const verification = await verifyInternalToken(token)
|
||||
|
||||
if (verification.valid) {
|
||||
let workflowId: string | null = null
|
||||
let userId: string | null = verification.userId || null
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
workflowId = searchParams.get('workflowId')
|
||||
if (!userId) {
|
||||
userId = searchParams.get('userId')
|
||||
}
|
||||
|
||||
if (!workflowId && !userId && request.method === 'POST') {
|
||||
try {
|
||||
// Clone the request to avoid consuming the original body
|
||||
const clonedRequest = request.clone()
|
||||
const bodyText = await clonedRequest.text()
|
||||
if (bodyText) {
|
||||
const body = JSON.parse(bodyText)
|
||||
workflowId = body.workflowId || body._context?.workflowId
|
||||
userId = userId || body.userId || body._context?.userId
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
return {
|
||||
success: true,
|
||||
userId,
|
||||
authType: 'internal_jwt',
|
||||
}
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
const [workflowData] = await db
|
||||
.select({ userId: workflow.userId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowData) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: workflowData.userId,
|
||||
authType: 'internal_jwt',
|
||||
}
|
||||
}
|
||||
|
||||
if (options.requireWorkflowId !== false) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'workflowId or userId required for internal JWT calls',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authType: 'internal_jwt',
|
||||
}
|
||||
return resolveUserFromJwt(request, verification.userId || null, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,5 @@ export type { AnonymousSession } from './anonymous'
|
||||
export { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
||||
export { auth, getSession, signIn, signUp } from './auth'
|
||||
export { ANONYMOUS_USER, ANONYMOUS_USER_ID } from './constants'
|
||||
export type { AuthResult } from './hybrid'
|
||||
export { checkHybridAuth, checkInternalAuth, checkSessionOrInternalAuth } from './hybrid'
|
||||
|
||||
Reference in New Issue
Block a user