d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
238 lines
6.7 KiB
TypeScript
238 lines
6.7 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import type { NextRequest } from 'next/server'
|
|
import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service'
|
|
import { getSession } from '@/lib/auth'
|
|
import { verifyInternalToken } from '@/lib/auth/internal'
|
|
|
|
const logger = createLogger('HybridAuth')
|
|
|
|
export const AuthType = {
|
|
SESSION: 'session',
|
|
API_KEY: 'api_key',
|
|
INTERNAL_JWT: 'internal_jwt',
|
|
} as const
|
|
|
|
export type AuthTypeValue = (typeof AuthType)[keyof typeof AuthType]
|
|
|
|
const API_KEY_HEADER = 'x-api-key'
|
|
const BEARER_PREFIX = 'Bearer '
|
|
|
|
/**
|
|
* Lightweight header-only check for whether a request carries external API credentials.
|
|
* Does NOT validate the credentials — only inspects headers to classify the request
|
|
* as programmatic API traffic vs interactive session traffic.
|
|
*/
|
|
export function hasExternalApiCredentials(headers: Headers): boolean {
|
|
if (headers.has(API_KEY_HEADER)) return true
|
|
const auth = headers.get('authorization')
|
|
return auth?.startsWith(BEARER_PREFIX) ?? false
|
|
}
|
|
|
|
export interface AuthResult {
|
|
success: boolean
|
|
userId?: string
|
|
workspaceId?: string
|
|
userName?: string | null
|
|
userEmail?: string | null
|
|
authType?: AuthTypeValue
|
|
apiKeyType?: 'personal' | 'workspace'
|
|
error?: string
|
|
}
|
|
|
|
/**
|
|
* Resolves userId from a verified internal JWT token.
|
|
* Only trusts the userId embedded in the JWT payload — never from user-controlled sources.
|
|
*/
|
|
function resolveUserFromJwt(
|
|
verificationUserId: string | null,
|
|
options: { requireWorkflowId?: boolean }
|
|
): AuthResult {
|
|
if (verificationUserId) {
|
|
return { success: true, userId: verificationUserId, authType: AuthType.INTERNAL_JWT }
|
|
}
|
|
|
|
if (options.requireWorkflowId !== false) {
|
|
return { success: false, error: 'userId required but not present in JWT' }
|
|
}
|
|
|
|
return { success: true, authType: 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(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(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,
|
|
userName: session.user.name,
|
|
userEmail: session.user.email,
|
|
authType: AuthType.SESSION,
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: 'Unauthorized',
|
|
}
|
|
} 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)
|
|
* 2. API key authentication (X-API-Key header)
|
|
* 3. Internal JWT authentication (Authorization: Bearer header)
|
|
*
|
|
* For internal JWT calls, requires workflowId to determine user context
|
|
*/
|
|
export async function checkHybridAuth(
|
|
request: NextRequest,
|
|
options: { requireWorkflowId?: boolean } = {}
|
|
): Promise<AuthResult> {
|
|
try {
|
|
// 1. Check for internal JWT token first
|
|
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(verification.userId || null, options)
|
|
}
|
|
}
|
|
|
|
// 2. Try session auth (for web UI)
|
|
const session = await getSession()
|
|
if (session?.user?.id) {
|
|
return {
|
|
success: true,
|
|
userId: session.user.id,
|
|
userName: session.user.name,
|
|
userEmail: session.user.email,
|
|
authType: AuthType.SESSION,
|
|
}
|
|
}
|
|
|
|
// 3. Try API key auth (X-API-Key header only)
|
|
const apiKeyHeader = request.headers.get('x-api-key')
|
|
if (apiKeyHeader) {
|
|
const result = await authenticateApiKeyFromHeader(apiKeyHeader)
|
|
if (result.success) {
|
|
await updateApiKeyLastUsed(result.keyId!)
|
|
return {
|
|
success: true,
|
|
userId: result.userId!,
|
|
workspaceId: result.workspaceId,
|
|
authType: AuthType.API_KEY,
|
|
apiKeyType: result.keyType,
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: 'Invalid API key',
|
|
}
|
|
}
|
|
|
|
// No authentication found
|
|
return {
|
|
success: false,
|
|
error: 'Unauthorized',
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error in hybrid authentication:', error)
|
|
return {
|
|
success: false,
|
|
error: 'Authentication error',
|
|
}
|
|
}
|
|
}
|