Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
139 changes: 96 additions & 43 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,90 @@ 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
if (
issuer.includes('okta.com') ||
issuer.includes('auth0.com') ||
issuer.includes('identityserver')
) {
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 hasExplicitEndpoints = authorizationEndpoint && tokenEndpoint && jwksEndpoint

logger.info('Using manual OIDC endpoints for provider', {
if (hasExplicitEndpoints) {
oidcConfig.authorizationEndpoint = authorizationEndpoint
oidcConfig.tokenEndpoint = tokenEndpoint
oidcConfig.userInfoEndpoint = userInfoEndpoint
oidcConfig.jwksEndpoint = jwksEndpoint

logger.info('Using explicitly provided OIDC endpoints', {
providerId,
provider: issuer.includes('okta.com')
? 'Okta'
: issuer.includes('auth0.com')
? 'Auth0'
: 'Generic',
authEndpoint: oidcConfig.authorizationEndpoint,
issuer,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
userInfoEndpoint: oidcConfig.userInfoEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
} else {
const discoveryUrl = `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`
try {
logger.info('Fetching OIDC discovery document', { discoveryUrl })

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}`,
},
{ status: 400 }
)
}

const discovery = await discoveryResponse.json()

if (
!discovery.authorization_endpoint ||
!discovery.token_endpoint ||
!discovery.jwks_uri
) {
logger.error('OIDC discovery document missing required endpoints', {
hasAuthEndpoint: !!discovery.authorization_endpoint,
hasTokenEndpoint: !!discovery.token_endpoint,
hasJwksUri: !!discovery.jwks_uri,
})
return NextResponse.json(
{
error:
'OIDC discovery document is missing required endpoints (authorization_endpoint, token_endpoint, jwks_uri)',
},
{ status: 400 }
)
}

oidcConfig.authorizationEndpoint = discovery.authorization_endpoint
oidcConfig.tokenEndpoint = discovery.token_endpoint
oidcConfig.userInfoEndpoint = discovery.userinfo_endpoint
oidcConfig.jwksEndpoint = discovery.jwks_uri
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

logger.info('Successfully fetched OIDC endpoints from 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.`,
},
{ 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