Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/app/api/auth/sso/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'

const logger = createLogger('SSO-Providers')
const logger = createLogger('SSOProvidersRoute')

export async function GET() {
try {
Expand Down
149 changes: 107 additions & 42 deletions apps/sim/app/api/auth/sso/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { hasSSOAccess } from '@/lib/billing'
import { env } from '@/lib/core/config/env'
import { REDACTED_MARKER } from '@/lib/core/security/redaction'

const logger = createLogger('SSO-Register')
const logger = createLogger('SSORegisterRoute')

const mappingSchema = z
.object({
Expand Down Expand Up @@ -43,6 +43,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
])
.default(['openid', 'profile', 'email']),
pkce: z.boolean().default(true),
authorizationEndpoint: z.string().url().optional(),
tokenEndpoint: z.string().url().optional(),
userInfoEndpoint: z.string().url().optional(),
jwksEndpoint: z.string().url().optional(),
}),
z.object({
providerType: z.literal('saml'),
Expand All @@ -64,12 +68,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [

export async function POST(request: NextRequest) {
try {
// SSO plugin must be enabled in Better Auth
if (!env.SSO_ENABLED) {
return NextResponse.json({ error: 'SSO is not enabled' }, { status: 400 })
}

// Check plan access (enterprise) or env var override
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
Expand Down Expand Up @@ -116,7 +118,16 @@ export async function POST(request: NextRequest) {
}

if (providerType === 'oidc') {
const { clientId, clientSecret, scopes, pkce } = body
const {
clientId,
clientSecret,
scopes,
pkce,
authorizationEndpoint,
tokenEndpoint,
userInfoEndpoint,
jwksEndpoint,
} = body

const oidcConfig: any = {
clientId,
Expand All @@ -127,48 +138,102 @@ export async function POST(request: NextRequest) {
pkce: pkce ?? true,
}

// Add manual endpoints for providers that might need them
// Common patterns for OIDC providers that don't support discovery properly
oidcConfig.authorizationEndpoint = authorizationEndpoint
oidcConfig.tokenEndpoint = tokenEndpoint
oidcConfig.userInfoEndpoint = userInfoEndpoint
oidcConfig.jwksEndpoint = jwksEndpoint

const needsDiscovery =
!oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint

if (needsDiscovery) {
const discoveryUrl = `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`
try {
logger.info('Fetching OIDC discovery document for missing endpoints', {
discoveryUrl,
hasAuthEndpoint: !!oidcConfig.authorizationEndpoint,
hasTokenEndpoint: !!oidcConfig.tokenEndpoint,
hasJwksEndpoint: !!oidcConfig.jwksEndpoint,
})

const discoveryResponse = await fetch(discoveryUrl, {
headers: { Accept: 'application/json' },
})

if (!discoveryResponse.ok) {
logger.error('Failed to fetch OIDC discovery document', {
status: discoveryResponse.status,
statusText: discoveryResponse.statusText,
})
return NextResponse.json(
{
error: `Failed to fetch OIDC discovery document from ${discoveryUrl}. Status: ${discoveryResponse.status}. Provide all endpoints explicitly or verify the issuer URL.`,
},
{ status: 400 }
)
}

const discovery = await discoveryResponse.json()

oidcConfig.authorizationEndpoint =
oidcConfig.authorizationEndpoint || discovery.authorization_endpoint
oidcConfig.tokenEndpoint = oidcConfig.tokenEndpoint || discovery.token_endpoint
oidcConfig.userInfoEndpoint = oidcConfig.userInfoEndpoint || discovery.userinfo_endpoint
oidcConfig.jwksEndpoint = oidcConfig.jwksEndpoint || discovery.jwks_uri

logger.info('Merged OIDC endpoints (user-provided + discovery)', {
providerId,
issuer,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
userInfoEndpoint: oidcConfig.userInfoEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
} catch (error) {
logger.error('Error fetching OIDC discovery document', {
error: error instanceof Error ? error.message : 'Unknown error',
discoveryUrl,
})
return NextResponse.json(
{
error: `Failed to fetch OIDC discovery document from ${discoveryUrl}. Please verify the issuer URL is correct or provide all endpoints explicitly.`,
},
{ status: 400 }
)
}
} else {
logger.info('Using explicitly provided OIDC endpoints (all present)', {
providerId,
issuer,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
userInfoEndpoint: oidcConfig.userInfoEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
}

if (
issuer.includes('okta.com') ||
issuer.includes('auth0.com') ||
issuer.includes('identityserver')
!oidcConfig.authorizationEndpoint ||
!oidcConfig.tokenEndpoint ||
!oidcConfig.jwksEndpoint
) {
const baseUrl = issuer.includes('/oauth2/default')
? issuer.replace('/oauth2/default', '')
: issuer.replace('/oauth', '').replace('/v2.0', '').replace('/oauth2', '')

// Okta-style endpoints
if (issuer.includes('okta.com')) {
oidcConfig.authorizationEndpoint = `${baseUrl}/oauth2/default/v1/authorize`
oidcConfig.tokenEndpoint = `${baseUrl}/oauth2/default/v1/token`
oidcConfig.userInfoEndpoint = `${baseUrl}/oauth2/default/v1/userinfo`
oidcConfig.jwksEndpoint = `${baseUrl}/oauth2/default/v1/keys`
}
// Auth0-style endpoints
else if (issuer.includes('auth0.com')) {
oidcConfig.authorizationEndpoint = `${baseUrl}/authorize`
oidcConfig.tokenEndpoint = `${baseUrl}/oauth/token`
oidcConfig.userInfoEndpoint = `${baseUrl}/userinfo`
oidcConfig.jwksEndpoint = `${baseUrl}/.well-known/jwks.json`
}
// Generic OIDC endpoints (IdentityServer, etc.)
else {
oidcConfig.authorizationEndpoint = `${baseUrl}/connect/authorize`
oidcConfig.tokenEndpoint = `${baseUrl}/connect/token`
oidcConfig.userInfoEndpoint = `${baseUrl}/connect/userinfo`
oidcConfig.jwksEndpoint = `${baseUrl}/.well-known/jwks`
}
const missing: string[] = []
if (!oidcConfig.authorizationEndpoint) missing.push('authorizationEndpoint')
if (!oidcConfig.tokenEndpoint) missing.push('tokenEndpoint')
if (!oidcConfig.jwksEndpoint) missing.push('jwksEndpoint')

logger.info('Using manual OIDC endpoints for provider', {
providerId,
provider: issuer.includes('okta.com')
? 'Okta'
: issuer.includes('auth0.com')
? 'Auth0'
: 'Generic',
authEndpoint: oidcConfig.authorizationEndpoint,
logger.error('Missing required OIDC endpoints after discovery merge', {
missing,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
return NextResponse.json(
{
error: `Missing required OIDC endpoints: ${missing.join(', ')}. Please provide these explicitly or verify the issuer supports OIDC discovery.`,
},
{ status: 400 }
)
}

providerConfig.oidcConfig = oidcConfig
Expand Down
8 changes: 2 additions & 6 deletions apps/sim/lib/copilot/tools/client/workflow/deploy-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInputFormatExample } from '@/lib/workflows/operations/deployment-utils'
import { useCopilotStore } from '@/stores/panel/copilot/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
Expand Down Expand Up @@ -36,7 +37,6 @@ export class DeployApiClientTool extends BaseClientTool {

const action = params?.action || 'deploy'

// Check if workflow is already deployed
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
const isAlreadyDeployed = workflowId
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
Expand Down Expand Up @@ -89,7 +89,6 @@ export class DeployApiClientTool extends BaseClientTool {
getDynamicText: (params, state) => {
const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy'

// Check if workflow is already deployed
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
const isAlreadyDeployed = workflowId
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
Expand Down Expand Up @@ -231,10 +230,7 @@ export class DeployApiClientTool extends BaseClientTool {
}

if (action === 'deploy') {
const appUrl =
typeof window !== 'undefined'
? window.location.origin
: process.env.NEXT_PUBLIC_APP_URL || 'https://app.sim.ai'
const appUrl = getBaseUrl()
const apiEndpoint = `${appUrl}/api/workflows/${workflowId}/execute`
const apiKeyPlaceholder = '$SIM_API_KEY'

Expand Down
38 changes: 0 additions & 38 deletions apps/sim/tools/http/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { isTest } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { transformTable } from '@/tools/shared/table'
import type { TableRow } from '@/tools/types'

const logger = createLogger('HTTPRequestUtils')

/**
* Creates a set of default headers used in HTTP requests
* @param customHeaders Additional user-provided headers to include
Expand All @@ -30,7 +26,6 @@ export const getDefaultHeaders = (
...customHeaders,
}

// Add Host header if not provided and URL is valid
if (url) {
try {
const hostname = new URL(url).host
Expand All @@ -57,26 +52,21 @@ export const processUrl = (
pathParams?: Record<string, string>,
queryParams?: TableRow[] | null
): string => {
// Strip any surrounding quotes
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
url = url.slice(1, -1)
}

// Replace path parameters
if (pathParams) {
Object.entries(pathParams).forEach(([key, value]) => {
url = url.replace(`:${key}`, encodeURIComponent(value))
})
}

// Handle query parameters
if (queryParams) {
const queryParamsObj = transformTable(queryParams)

// Verify if URL already has query params to use proper separator
const separator = url.includes('?') ? '&' : '?'

// Build query string manually to avoid double-encoding issues
const queryParts: string[] = []

for (const [key, value] of Object.entries(queryParamsObj)) {
Expand All @@ -92,31 +82,3 @@ export const processUrl = (

return url
}

// Check if a URL needs proxy to avoid CORS/method restrictions
export const shouldUseProxy = (url: string): boolean => {
// Skip proxying in test environment
if (isTest) {
return false
}

// Only consider proxying in browser environment
if (typeof window === 'undefined') {
return false
}

try {
const _urlObj = new URL(url)
const currentOrigin = window.location.origin

// Don't proxy same-origin or localhost requests
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
return false
}

return true // Proxy all cross-origin requests for consistency
} catch (e) {
logger.warn('URL parsing failed:', e)
return false
}
}
8 changes: 0 additions & 8 deletions packages/db/scripts/deregister-sso-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { ssoProvider, user } from '../schema'

// Simple console logger
const logger = {
info: (message: string, meta?: any) => {
const timestamp = new Date().toISOString()
Expand All @@ -43,7 +42,6 @@ const logger = {
},
}

// Get database URL from environment
const CONNECTION_STRING = process.env.POSTGRES_URL ?? process.env.DATABASE_URL
if (!CONNECTION_STRING) {
console.error('❌ POSTGRES_URL or DATABASE_URL environment variable is required')
Expand Down Expand Up @@ -88,15 +86,13 @@ async function deregisterSSOProvider(): Promise<boolean> {
return false
}

// Get user
const targetUser = await getUser(userEmail)
if (!targetUser) {
return false
}

logger.info(`Found user: ${targetUser.email} (ID: ${targetUser.id})`)

// Get SSO providers for this user
const providers = await db
.select()
.from(ssoProvider)
Expand All @@ -112,11 +108,9 @@ async function deregisterSSOProvider(): Promise<boolean> {
logger.info(` - Provider ID: ${provider.providerId}, Domain: ${provider.domain}`)
}

// Check if specific provider ID was requested
const specificProviderId = process.env.SSO_PROVIDER_ID

if (specificProviderId) {
// Delete specific provider
const providerToDelete = providers.find((p) => p.providerId === specificProviderId)
if (!providerToDelete) {
logger.error(`Provider '${specificProviderId}' not found for user ${targetUser.email}`)
Expand All @@ -133,7 +127,6 @@ async function deregisterSSOProvider(): Promise<boolean> {
`✅ Successfully deleted SSO provider '${specificProviderId}' for user ${targetUser.email}`
)
} else {
// Delete all providers for this user
await db.delete(ssoProvider).where(eq(ssoProvider.userId, targetUser.id))

logger.info(
Expand Down Expand Up @@ -171,7 +164,6 @@ async function main() {
}
}

// Handle script execution
main().catch((error) => {
logger.error('Script execution failed:', { error })
process.exit(1)
Expand Down
Loading