mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(sso): removed provider specific OIDC logic from SSO registration & deregistration scripts (#2896)
* fix(sso): updated registration & deregistration script for explicit support for Entra ID * cleanup * ack PR comment * ack PR comment * tested edge cases, ack'd PR comments * remove trailing slash
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
@@ -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'),
|
||||
@@ -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 })
|
||||
@@ -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,
|
||||
@@ -127,50 +138,104 @@ 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', '')
|
||||
oidcConfig.authorizationEndpoint = authorizationEndpoint
|
||||
oidcConfig.tokenEndpoint = tokenEndpoint
|
||||
oidcConfig.userInfoEndpoint = userInfoEndpoint
|
||||
oidcConfig.jwksEndpoint = jwksEndpoint
|
||||
|
||||
// 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 needsDiscovery =
|
||||
!oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint
|
||||
|
||||
logger.info('Using manual OIDC endpoints for provider', {
|
||||
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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
!oidcConfig.authorizationEndpoint ||
|
||||
!oidcConfig.tokenEndpoint ||
|
||||
!oidcConfig.jwksEndpoint
|
||||
) {
|
||||
const missing: string[] = []
|
||||
if (!oidcConfig.authorizationEndpoint) missing.push('authorizationEndpoint')
|
||||
if (!oidcConfig.tokenEndpoint) missing.push('tokenEndpoint')
|
||||
if (!oidcConfig.jwksEndpoint) missing.push('jwksEndpoint')
|
||||
|
||||
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
|
||||
} else if (providerType === 'saml') {
|
||||
const {
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)) {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -88,7 +86,6 @@ async function deregisterSSOProvider(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get user
|
||||
const targetUser = await getUser(userEmail)
|
||||
if (!targetUser) {
|
||||
return false
|
||||
@@ -96,7 +93,6 @@ async function deregisterSSOProvider(): Promise<boolean> {
|
||||
|
||||
logger.info(`Found user: ${targetUser.email} (ID: ${targetUser.id})`)
|
||||
|
||||
// Get SSO providers for this user
|
||||
const providers = await db
|
||||
.select()
|
||||
.from(ssoProvider)
|
||||
@@ -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}`)
|
||||
@@ -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(
|
||||
@@ -171,7 +164,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle script execution
|
||||
main().catch((error) => {
|
||||
logger.error('Script execution failed:', { error })
|
||||
process.exit(1)
|
||||
|
||||
@@ -38,7 +38,6 @@ import postgres from 'postgres'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ssoProvider, user } from '../schema'
|
||||
|
||||
// Self-contained SSO types (matching Better Auth's structure)
|
||||
interface SSOMapping {
|
||||
id: string
|
||||
email: string
|
||||
@@ -108,7 +107,6 @@ interface SSOProviderConfig {
|
||||
samlConfig?: SAMLConfig
|
||||
}
|
||||
|
||||
// Simple console logger (no dependencies)
|
||||
const logger = {
|
||||
info: (message: string, meta?: any) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
@@ -133,14 +131,12 @@ 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')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Initialize database connection (following migration script pattern)
|
||||
const postgresClient = postgres(CONNECTION_STRING, {
|
||||
prepare: false,
|
||||
idle_timeout: 20,
|
||||
@@ -161,7 +157,6 @@ interface SSOProviderData {
|
||||
organizationId?: string
|
||||
}
|
||||
|
||||
// Self-contained configuration builder (no external dependencies)
|
||||
function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
const enabled = process.env.SSO_ENABLED === 'true'
|
||||
if (!enabled) return null
|
||||
@@ -182,7 +177,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
providerType,
|
||||
}
|
||||
|
||||
// Build field mapping
|
||||
config.mapping = {
|
||||
id:
|
||||
process.env.SSO_MAPPING_ID ||
|
||||
@@ -202,7 +196,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
image: process.env.SSO_MAPPING_IMAGE || (providerType === 'oidc' ? 'picture' : undefined),
|
||||
}
|
||||
|
||||
// Build provider-specific configuration
|
||||
if (providerType === 'oidc') {
|
||||
const clientId = process.env.SSO_OIDC_CLIENT_ID
|
||||
const clientSecret = process.env.SSO_OIDC_CLIENT_SECRET
|
||||
@@ -225,7 +218,8 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
userInfoEndpoint: process.env.SSO_OIDC_USERINFO_ENDPOINT,
|
||||
jwksEndpoint: process.env.SSO_OIDC_JWKS_ENDPOINT,
|
||||
discoveryEndpoint:
|
||||
process.env.SSO_OIDC_DISCOVERY_ENDPOINT || `${issuer}/.well-known/openid-configuration`,
|
||||
process.env.SSO_OIDC_DISCOVERY_ENDPOINT ||
|
||||
`${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`,
|
||||
}
|
||||
} else if (providerType === 'saml') {
|
||||
const entryPoint = process.env.SSO_SAML_ENTRY_POINT
|
||||
@@ -237,7 +231,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
|
||||
const callbackUrl = process.env.SSO_SAML_CALLBACK_URL || `${issuer}/callback`
|
||||
|
||||
// Use custom metadata if provided, otherwise generate default
|
||||
let spMetadata = process.env.SSO_SAML_SP_METADATA
|
||||
if (!spMetadata) {
|
||||
spMetadata = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -263,7 +256,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
entityID: issuer,
|
||||
},
|
||||
}
|
||||
// Optionally include IDP metadata if provided
|
||||
const idpMetadata = process.env.SSO_SAML_IDP_METADATA
|
||||
if (idpMetadata) {
|
||||
config.samlConfig.idpMetadata = {
|
||||
@@ -275,7 +267,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
|
||||
return config
|
||||
}
|
||||
|
||||
// Self-contained example environment variables function
|
||||
function getExampleEnvVars(
|
||||
providerType: 'oidc' | 'saml',
|
||||
provider?: string
|
||||
@@ -358,7 +349,6 @@ async function getAdminUser(): Promise<{ id: string; email: string } | null> {
|
||||
|
||||
async function registerSSOProvider(): Promise<boolean> {
|
||||
try {
|
||||
// Build configuration from environment variables
|
||||
const ssoConfig = buildSSOConfigFromEnv()
|
||||
|
||||
if (!ssoConfig) {
|
||||
@@ -381,7 +371,6 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get admin user
|
||||
const adminUser = await getAdminUser()
|
||||
if (!adminUser) {
|
||||
return false
|
||||
@@ -394,7 +383,6 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
adminUser: adminUser.email,
|
||||
})
|
||||
|
||||
// Validate issuer URL (same as Better Auth does)
|
||||
try {
|
||||
new URL(ssoConfig.issuer)
|
||||
} catch {
|
||||
@@ -402,7 +390,97 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if provider already exists
|
||||
if (ssoConfig.providerType === 'oidc' && ssoConfig.oidcConfig) {
|
||||
const needsDiscovery =
|
||||
!ssoConfig.oidcConfig.authorizationEndpoint ||
|
||||
!ssoConfig.oidcConfig.tokenEndpoint ||
|
||||
!ssoConfig.oidcConfig.jwksEndpoint
|
||||
|
||||
if (needsDiscovery) {
|
||||
const discoveryUrl =
|
||||
ssoConfig.oidcConfig.discoveryEndpoint ||
|
||||
`${ssoConfig.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`
|
||||
logger.info('Fetching OIDC discovery document for missing endpoints...', {
|
||||
discoveryUrl,
|
||||
hasAuthEndpoint: !!ssoConfig.oidcConfig.authorizationEndpoint,
|
||||
hasTokenEndpoint: !!ssoConfig.oidcConfig.tokenEndpoint,
|
||||
hasJwksEndpoint: !!ssoConfig.oidcConfig.jwksEndpoint,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(discoveryUrl, {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to fetch OIDC discovery document', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
logger.error(
|
||||
'Provide all endpoints explicitly via SSO_OIDC_AUTHORIZATION_ENDPOINT, SSO_OIDC_TOKEN_ENDPOINT, SSO_OIDC_JWKS_ENDPOINT'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const discovery = await response.json()
|
||||
|
||||
ssoConfig.oidcConfig.authorizationEndpoint =
|
||||
ssoConfig.oidcConfig.authorizationEndpoint || discovery.authorization_endpoint
|
||||
ssoConfig.oidcConfig.tokenEndpoint =
|
||||
ssoConfig.oidcConfig.tokenEndpoint || discovery.token_endpoint
|
||||
ssoConfig.oidcConfig.userInfoEndpoint =
|
||||
ssoConfig.oidcConfig.userInfoEndpoint || discovery.userinfo_endpoint
|
||||
ssoConfig.oidcConfig.jwksEndpoint =
|
||||
ssoConfig.oidcConfig.jwksEndpoint || discovery.jwks_uri
|
||||
|
||||
logger.info('Merged OIDC endpoints (user-provided + discovery)', {
|
||||
authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint,
|
||||
tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint,
|
||||
userInfoEndpoint: ssoConfig.oidcConfig.userInfoEndpoint,
|
||||
jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching OIDC discovery document', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
discoveryUrl,
|
||||
})
|
||||
logger.error(
|
||||
'Please provide explicit endpoints via SSO_OIDC_AUTHORIZATION_ENDPOINT, SSO_OIDC_TOKEN_ENDPOINT, SSO_OIDC_JWKS_ENDPOINT'
|
||||
)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
logger.info('Using explicitly provided OIDC endpoints (all present)', {
|
||||
authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint,
|
||||
tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint,
|
||||
userInfoEndpoint: ssoConfig.oidcConfig.userInfoEndpoint,
|
||||
jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
!ssoConfig.oidcConfig.authorizationEndpoint ||
|
||||
!ssoConfig.oidcConfig.tokenEndpoint ||
|
||||
!ssoConfig.oidcConfig.jwksEndpoint
|
||||
) {
|
||||
const missing: string[] = []
|
||||
if (!ssoConfig.oidcConfig.authorizationEndpoint)
|
||||
missing.push('SSO_OIDC_AUTHORIZATION_ENDPOINT')
|
||||
if (!ssoConfig.oidcConfig.tokenEndpoint) missing.push('SSO_OIDC_TOKEN_ENDPOINT')
|
||||
if (!ssoConfig.oidcConfig.jwksEndpoint) missing.push('SSO_OIDC_JWKS_ENDPOINT')
|
||||
|
||||
logger.error('Missing required OIDC endpoints after discovery merge', {
|
||||
missing,
|
||||
authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint,
|
||||
tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint,
|
||||
jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint,
|
||||
})
|
||||
logger.error(`Please provide: ${missing.join(', ')}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const existingProviders = await db
|
||||
.select()
|
||||
.from(ssoProvider)
|
||||
@@ -413,9 +491,8 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
logger.info('Updating existing provider...')
|
||||
}
|
||||
|
||||
// Build provider data (following Better Auth's exact structure)
|
||||
const providerData: SSOProviderData = {
|
||||
id: uuidv4(), // Generate unique ID
|
||||
id: uuidv4(),
|
||||
issuer: ssoConfig.issuer,
|
||||
domain: ssoConfig.domain,
|
||||
userId: adminUser.id,
|
||||
@@ -423,7 +500,6 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
organizationId: process.env.SSO_ORGANIZATION_ID || undefined,
|
||||
}
|
||||
|
||||
// Build OIDC config (same as Better Auth endpoint)
|
||||
if (ssoConfig.providerType === 'oidc' && ssoConfig.oidcConfig) {
|
||||
const oidcConfig = {
|
||||
issuer: ssoConfig.issuer,
|
||||
@@ -445,7 +521,6 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
providerData.oidcConfig = JSON.stringify(oidcConfig)
|
||||
}
|
||||
|
||||
// Build SAML config (same as Better Auth endpoint)
|
||||
if (ssoConfig.providerType === 'saml' && ssoConfig.samlConfig) {
|
||||
const samlConfig = {
|
||||
issuer: ssoConfig.issuer,
|
||||
@@ -467,7 +542,6 @@ async function registerSSOProvider(): Promise<boolean> {
|
||||
providerData.samlConfig = JSON.stringify(samlConfig)
|
||||
}
|
||||
|
||||
// Insert or update the SSO provider record
|
||||
if (existingProviders.length > 0) {
|
||||
await db
|
||||
.update(ssoProvider)
|
||||
@@ -521,7 +595,6 @@ async function main() {
|
||||
console.log('This script directly inserts SSO provider records into the database.')
|
||||
console.log("It follows Better Auth's exact registerSSOProvider logic.\n")
|
||||
|
||||
// Register the SSO provider using direct database access
|
||||
const success = await registerSSOProvider()
|
||||
|
||||
if (success) {
|
||||
@@ -538,7 +611,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle script execution
|
||||
main().catch((error) => {
|
||||
logger.error('Script execution failed:', { error })
|
||||
process.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user