Skip to content

Commit 94ead30

Browse files
committed
fix(sso): updated registration & deregistration script for explicit support for Entra ID
1 parent 69614d2 commit 94ead30

File tree

6 files changed

+161
-116
lines changed

6 files changed

+161
-116
lines changed

apps/sim/app/api/auth/sso/providers/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm'
44
import { NextResponse } from 'next/server'
55
import { getSession } from '@/lib/auth'
66

7-
const logger = createLogger('SSO-Providers')
7+
const logger = createLogger('SSOProvidersRoute')
88

99
export async function GET() {
1010
try {

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { hasSSOAccess } from '@/lib/billing'
66
import { env } from '@/lib/core/config/env'
77
import { REDACTED_MARKER } from '@/lib/core/security/redaction'
88

9-
const logger = createLogger('SSO-Register')
9+
const logger = createLogger('SSORegisterRoute')
1010

1111
const mappingSchema = z
1212
.object({
@@ -43,6 +43,11 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
4343
])
4444
.default(['openid', 'profile', 'email']),
4545
pkce: z.boolean().default(true),
46+
// Optional explicit endpoints - if not provided, fetched from OIDC discovery
47+
authorizationEndpoint: z.string().url().optional(),
48+
tokenEndpoint: z.string().url().optional(),
49+
userInfoEndpoint: z.string().url().optional(),
50+
jwksEndpoint: z.string().url().optional(),
4651
}),
4752
z.object({
4853
providerType: z.literal('saml'),
@@ -64,12 +69,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
6469

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

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

118121
if (providerType === 'oidc') {
119-
const { clientId, clientSecret, scopes, pkce } = body
122+
const {
123+
clientId,
124+
clientSecret,
125+
scopes,
126+
pkce,
127+
authorizationEndpoint,
128+
tokenEndpoint,
129+
userInfoEndpoint,
130+
jwksEndpoint,
131+
} = body
120132

121133
const oidcConfig: any = {
122134
clientId,
@@ -127,48 +139,90 @@ export async function POST(request: NextRequest) {
127139
pkce: pkce ?? true,
128140
}
129141

130-
// Add manual endpoints for providers that might need them
131-
// Common patterns for OIDC providers that don't support discovery properly
132-
if (
133-
issuer.includes('okta.com') ||
134-
issuer.includes('auth0.com') ||
135-
issuer.includes('identityserver')
136-
) {
137-
const baseUrl = issuer.includes('/oauth2/default')
138-
? issuer.replace('/oauth2/default', '')
139-
: issuer.replace('/oauth', '').replace('/v2.0', '').replace('/oauth2', '')
140-
141-
// Okta-style endpoints
142-
if (issuer.includes('okta.com')) {
143-
oidcConfig.authorizationEndpoint = `${baseUrl}/oauth2/default/v1/authorize`
144-
oidcConfig.tokenEndpoint = `${baseUrl}/oauth2/default/v1/token`
145-
oidcConfig.userInfoEndpoint = `${baseUrl}/oauth2/default/v1/userinfo`
146-
oidcConfig.jwksEndpoint = `${baseUrl}/oauth2/default/v1/keys`
147-
}
148-
// Auth0-style endpoints
149-
else if (issuer.includes('auth0.com')) {
150-
oidcConfig.authorizationEndpoint = `${baseUrl}/authorize`
151-
oidcConfig.tokenEndpoint = `${baseUrl}/oauth/token`
152-
oidcConfig.userInfoEndpoint = `${baseUrl}/userinfo`
153-
oidcConfig.jwksEndpoint = `${baseUrl}/.well-known/jwks.json`
154-
}
155-
// Generic OIDC endpoints (IdentityServer, etc.)
156-
else {
157-
oidcConfig.authorizationEndpoint = `${baseUrl}/connect/authorize`
158-
oidcConfig.tokenEndpoint = `${baseUrl}/connect/token`
159-
oidcConfig.userInfoEndpoint = `${baseUrl}/connect/userinfo`
160-
oidcConfig.jwksEndpoint = `${baseUrl}/.well-known/jwks`
161-
}
142+
const hasExplicitEndpoints = authorizationEndpoint && tokenEndpoint && jwksEndpoint
162143

163-
logger.info('Using manual OIDC endpoints for provider', {
144+
if (hasExplicitEndpoints) {
145+
oidcConfig.authorizationEndpoint = authorizationEndpoint
146+
oidcConfig.tokenEndpoint = tokenEndpoint
147+
oidcConfig.userInfoEndpoint = userInfoEndpoint
148+
oidcConfig.jwksEndpoint = jwksEndpoint
149+
150+
logger.info('Using explicitly provided OIDC endpoints', {
164151
providerId,
165-
provider: issuer.includes('okta.com')
166-
? 'Okta'
167-
: issuer.includes('auth0.com')
168-
? 'Auth0'
169-
: 'Generic',
170-
authEndpoint: oidcConfig.authorizationEndpoint,
152+
issuer,
153+
authorizationEndpoint: oidcConfig.authorizationEndpoint,
154+
tokenEndpoint: oidcConfig.tokenEndpoint,
155+
userInfoEndpoint: oidcConfig.userInfoEndpoint,
156+
jwksEndpoint: oidcConfig.jwksEndpoint,
171157
})
158+
} else {
159+
const discoveryUrl = `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`
160+
try {
161+
logger.info('Fetching OIDC discovery document', { discoveryUrl })
162+
163+
const discoveryResponse = await fetch(discoveryUrl, {
164+
headers: { Accept: 'application/json' },
165+
})
166+
167+
if (!discoveryResponse.ok) {
168+
logger.error('Failed to fetch OIDC discovery document', {
169+
status: discoveryResponse.status,
170+
statusText: discoveryResponse.statusText,
171+
})
172+
return NextResponse.json(
173+
{
174+
error: `Failed to fetch OIDC discovery document from ${discoveryUrl}. Status: ${discoveryResponse.status}`,
175+
},
176+
{ status: 400 }
177+
)
178+
}
179+
180+
const discovery = await discoveryResponse.json()
181+
182+
if (
183+
!discovery.authorization_endpoint ||
184+
!discovery.token_endpoint ||
185+
!discovery.jwks_uri
186+
) {
187+
logger.error('OIDC discovery document missing required endpoints', {
188+
hasAuthEndpoint: !!discovery.authorization_endpoint,
189+
hasTokenEndpoint: !!discovery.token_endpoint,
190+
hasJwksUri: !!discovery.jwks_uri,
191+
})
192+
return NextResponse.json(
193+
{
194+
error:
195+
'OIDC discovery document is missing required endpoints (authorization_endpoint, token_endpoint, jwks_uri)',
196+
},
197+
{ status: 400 }
198+
)
199+
}
200+
201+
oidcConfig.authorizationEndpoint = discovery.authorization_endpoint
202+
oidcConfig.tokenEndpoint = discovery.token_endpoint
203+
oidcConfig.userInfoEndpoint = discovery.userinfo_endpoint
204+
oidcConfig.jwksEndpoint = discovery.jwks_uri
205+
206+
logger.info('Successfully fetched OIDC endpoints from discovery', {
207+
providerId,
208+
issuer,
209+
authorizationEndpoint: oidcConfig.authorizationEndpoint,
210+
tokenEndpoint: oidcConfig.tokenEndpoint,
211+
userInfoEndpoint: oidcConfig.userInfoEndpoint,
212+
jwksEndpoint: oidcConfig.jwksEndpoint,
213+
})
214+
} catch (error) {
215+
logger.error('Error fetching OIDC discovery document', {
216+
error: error instanceof Error ? error.message : 'Unknown error',
217+
discoveryUrl,
218+
})
219+
return NextResponse.json(
220+
{
221+
error: `Failed to fetch OIDC discovery document from ${discoveryUrl}. Please verify the issuer URL is correct.`,
222+
},
223+
{ status: 400 }
224+
)
225+
}
172226
}
173227

174228
providerConfig.oidcConfig = oidcConfig

apps/sim/lib/copilot/tools/client/workflow/deploy-api.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ClientToolCallState,
77
} from '@/lib/copilot/tools/client/base-tool'
88
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
9+
import { getBaseUrl } from '@/lib/core/utils/urls'
910
import { getInputFormatExample } from '@/lib/workflows/operations/deployment-utils'
1011
import { useCopilotStore } from '@/stores/panel/copilot/store'
1112
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -36,7 +37,6 @@ export class DeployApiClientTool extends BaseClientTool {
3637

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

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

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

233232
if (action === 'deploy') {
234-
const appUrl =
235-
typeof window !== 'undefined'
236-
? window.location.origin
237-
: process.env.NEXT_PUBLIC_APP_URL || 'https://app.sim.ai'
233+
const appUrl = getBaseUrl()
238234
const apiEndpoint = `${appUrl}/api/workflows/${workflowId}/execute`
239235
const apiKeyPlaceholder = '$SIM_API_KEY'
240236

apps/sim/tools/http/utils.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { createLogger } from '@sim/logger'
2-
import { isTest } from '@/lib/core/config/feature-flags'
31
import { getBaseUrl } from '@/lib/core/utils/urls'
42
import { transformTable } from '@/tools/shared/table'
53
import type { TableRow } from '@/tools/types'
64

7-
const logger = createLogger('HTTPRequestUtils')
8-
95
/**
106
* Creates a set of default headers used in HTTP requests
117
* @param customHeaders Additional user-provided headers to include
@@ -30,7 +26,6 @@ export const getDefaultHeaders = (
3026
...customHeaders,
3127
}
3228

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

65-
// Replace path parameters
6659
if (pathParams) {
6760
Object.entries(pathParams).forEach(([key, value]) => {
6861
url = url.replace(`:${key}`, encodeURIComponent(value))
6962
})
7063
}
7164

72-
// Handle query parameters
7365
if (queryParams) {
7466
const queryParamsObj = transformTable(queryParams)
7567

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

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

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

9383
return url
9484
}
95-
96-
// Check if a URL needs proxy to avoid CORS/method restrictions
97-
export const shouldUseProxy = (url: string): boolean => {
98-
// Skip proxying in test environment
99-
if (isTest) {
100-
return false
101-
}
102-
103-
// Only consider proxying in browser environment
104-
if (typeof window === 'undefined') {
105-
return false
106-
}
107-
108-
try {
109-
const _urlObj = new URL(url)
110-
const currentOrigin = window.location.origin
111-
112-
// Don't proxy same-origin or localhost requests
113-
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
114-
return false
115-
}
116-
117-
return true // Proxy all cross-origin requests for consistency
118-
} catch (e) {
119-
logger.warn('URL parsing failed:', e)
120-
return false
121-
}
122-
}

packages/db/scripts/deregister-sso-provider.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { drizzle } from 'drizzle-orm/postgres-js'
1818
import postgres from 'postgres'
1919
import { ssoProvider, user } from '../schema'
2020

21-
// Simple console logger
2221
const logger = {
2322
info: (message: string, meta?: any) => {
2423
const timestamp = new Date().toISOString()
@@ -43,7 +42,6 @@ const logger = {
4342
},
4443
}
4544

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

91-
// Get user
9289
const targetUser = await getUser(userEmail)
9390
if (!targetUser) {
9491
return false
9592
}
9693

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

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

115-
// Check if specific provider ID was requested
116111
const specificProviderId = process.env.SSO_PROVIDER_ID
117112

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

139132
logger.info(
@@ -171,7 +164,6 @@ async function main() {
171164
}
172165
}
173166

174-
// Handle script execution
175167
main().catch((error) => {
176168
logger.error('Script execution failed:', { error })
177169
process.exit(1)

0 commit comments

Comments
 (0)