- {/* Form fields skeleton */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {Array.from({ length: 5 }).map((_, i) => (
+
+
+
+
+ ))}
-
- {/* Footer skeleton */}
diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts
index 37c20ffb7fb..b7e53ef86b5 100644
--- a/apps/sim/ee/sso/hooks/sso.ts
+++ b/apps/sim/ee/sso/hooks/sso.ts
@@ -14,8 +14,11 @@ export const ssoKeys = {
/**
* Fetch SSO providers
*/
-async function fetchSSOProviders(signal: AbortSignal) {
- const response = await fetch('/api/auth/sso/providers', { signal })
+async function fetchSSOProviders(signal: AbortSignal, organizationId?: string) {
+ const url = organizationId
+ ? `/api/auth/sso/providers?organizationId=${encodeURIComponent(organizationId)}`
+ : '/api/auth/sso/providers'
+ const response = await fetch(url, { signal })
if (!response.ok) {
throw new Error('Failed to fetch SSO providers')
}
@@ -27,12 +30,13 @@ async function fetchSSOProviders(signal: AbortSignal) {
*/
interface UseSSOProvidersOptions {
enabled?: boolean
+ organizationId?: string
}
-export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {}) {
+export function useSSOProviders({ enabled = true, organizationId }: UseSSOProvidersOptions = {}) {
return useQuery({
- queryKey: ssoKeys.providers(),
- queryFn: ({ signal }) => fetchSSOProviders(signal),
+ queryKey: [...ssoKeys.providers(), organizationId ?? ''],
+ queryFn: ({ signal }) => fetchSSOProviders(signal, organizationId),
staleTime: 5 * 60 * 1000,
placeholderData: keepPreviousData,
enabled,
@@ -42,13 +46,7 @@ export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {})
/**
* Configure SSO provider mutation
*/
-interface ConfigureSSOParams {
- provider: string
- domain: string
- clientId: string
- clientSecret: string
- orgId?: string
-}
+type ConfigureSSOParams = Record
export function useConfigureSSO() {
const queryClient = useQueryClient()
@@ -63,21 +61,18 @@ export function useConfigureSSO() {
if (!response.ok) {
const error = await response.json()
- throw new Error(error.message || 'Failed to configure SSO')
+ throw new Error(error.error || error.details || 'Failed to configure SSO')
}
return response.json()
},
- onSuccess: (_data, variables) => {
+ onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: ssoKeys.providers() })
- if (variables.orgId) {
- queryClient.invalidateQueries({
- queryKey: organizationKeys.detail(variables.orgId),
- })
- queryClient.invalidateQueries({
- queryKey: organizationKeys.lists(),
- })
+ const orgId = typeof variables.orgId === 'string' ? variables.orgId : undefined
+ if (orgId) {
+ queryClient.invalidateQueries({ queryKey: organizationKeys.detail(orgId) })
+ queryClient.invalidateQueries({ queryKey: organizationKeys.lists() })
}
},
})
diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts
index fba33becdac..d0c93c598f4 100644
--- a/apps/sim/lib/auth/auth.ts
+++ b/apps/sim/lib/auth/auth.ts
@@ -2831,7 +2831,16 @@ export const auth = betterAuth({
],
}),
// Include SSO plugin when enabled
- ...(env.SSO_ENABLED ? [sso()] : []),
+ ...(env.SSO_ENABLED
+ ? [
+ sso({
+ organizationProvisioning: {
+ disabled: false,
+ defaultRole: 'member',
+ },
+ }),
+ ]
+ : []),
// Only include the Stripe plugin when billing is enabled
...(isBillingEnabled && stripeClient
? [
diff --git a/packages/db/scripts/register-sso-provider.ts b/packages/db/scripts/register-sso-provider.ts
index ed00894efce..19052ece762 100644
--- a/packages/db/scripts/register-sso-provider.ts
+++ b/packages/db/scripts/register-sso-provider.ts
@@ -235,18 +235,69 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
return null
}
- const callbackUrl = process.env.SSO_SAML_CALLBACK_URL || `${issuer}/callback`
+ const appBaseUrl = (
+ process.env.NEXT_PUBLIC_APP_URL ||
+ process.env.BETTER_AUTH_URL ||
+ ''
+ ).replace(/\/$/, '')
+
+ const escapeXml = (str: string) =>
+ str.replace(/[<>&"']/g, (c) => {
+ switch (c) {
+ case '<':
+ return '<'
+ case '>':
+ return '>'
+ case '&':
+ return '&'
+ case '"':
+ return '"'
+ case "'":
+ return '''
+ default:
+ return c
+ }
+ })
+
+ const callbackUrl =
+ process.env.SSO_SAML_CALLBACK_URL || `${appBaseUrl}/api/auth/sso/saml2/callback/${providerId}`
let spMetadata = process.env.SSO_SAML_SP_METADATA
if (!spMetadata) {
spMetadata = `
-
+
-
+
`
}
+ const idpMetadataXml = process.env.SSO_SAML_IDP_METADATA
+ let computedIdpMetadata: string
+ if (idpMetadataXml) {
+ computedIdpMetadata = idpMetadataXml
+ } else {
+ const certBase64 = cert
+ .replace(/-----BEGIN CERTIFICATE-----/g, '')
+ .replace(/-----END CERTIFICATE-----/g, '')
+ .replace(/\s/g, '')
+ const escapedEntryPoint = escapeXml(entryPoint)
+ computedIdpMetadata = `
+
+
+
+
+
+ ${certBase64}
+
+
+
+
+
+
+`
+ }
+
config.samlConfig = {
issuer,
entryPoint,
@@ -259,14 +310,11 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
identifierFormat: process.env.SSO_SAML_IDENTIFIER_FORMAT,
spMetadata: {
metadata: spMetadata,
- entityID: issuer,
+ entityID: appBaseUrl,
+ },
+ idpMetadata: {
+ metadata: computedIdpMetadata,
},
- }
- const idpMetadata = process.env.SSO_SAML_IDP_METADATA
- if (idpMetadata) {
- config.samlConfig.idpMetadata = {
- metadata: idpMetadata,
- }
}
}
@@ -396,6 +444,17 @@ async function registerSSOProvider(): Promise {
return false
}
+ if (
+ ssoConfig.providerType === 'saml' &&
+ !process.env.NEXT_PUBLIC_APP_URL &&
+ !process.env.BETTER_AUTH_URL
+ ) {
+ logger.error(
+ 'NEXT_PUBLIC_APP_URL or BETTER_AUTH_URL is required for SAML — it is used as the SP entity ID in SP metadata. Set one of these env vars.'
+ )
+ return false
+ }
+
if (ssoConfig.providerType === 'oidc' && ssoConfig.oidcConfig) {
const needsDiscovery =
!ssoConfig.oidcConfig.authorizationEndpoint ||
@@ -579,8 +638,13 @@ async function registerSSOProvider(): Promise {
const baseUrl =
process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || 'https://your-domain.com'
- const callbackUrl = `${baseUrl}/api/auth/sso/callback/${ssoConfig.providerId}`
- logger.info(`📋 Callback URL (configure this in your identity provider): ${callbackUrl}`)
+ const callbackPath =
+ ssoConfig.providerType === 'saml'
+ ? `api/auth/sso/saml2/callback/${ssoConfig.providerId}`
+ : `api/auth/sso/callback/${ssoConfig.providerId}`
+ logger.info(
+ `📋 Callback URL (configure this in your identity provider): ${baseUrl}/${callbackPath}`
+ )
return true
} catch (error) {