From 97594f1f7419a6c99b98f974fc7b0da9f098a345 Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Fri, 19 Sep 2025 23:41:21 -0700 Subject: [PATCH 01/13] feat(sso): added login with SAML/SSO --- .../(auth)/components/sso-login-button.tsx | 128 + apps/sim/app/(auth)/login/login-form.tsx | 208 +- apps/sim/app/(auth)/signup/signup-form.tsx | 254 +- apps/sim/app/api/auth/sso/providers/route.ts | 67 + apps/sim/app/api/auth/sso/register/route.ts | 195 + .../settings-modal/components/index.ts | 1 + .../settings-navigation.tsx | 31 +- .../settings-modal/components/sso/sso.tsx | 828 ++++ .../settings-modal/settings-modal.tsx | 7 + apps/sim/lib/auth-client.ts | 3 + apps/sim/lib/auth.ts | 52 + apps/sim/lib/env.ts | 8 + apps/sim/lib/environment.ts | 6 +- apps/sim/package.json | 1 + bun.lock | 3817 ----------------- .../db/migrations/meta/0091_snapshot.json | 2 +- packages/db/migrations/meta/_journal.json | 2 +- packages/db/schema.ts | 25 + packages/db/scripts/register-sso-provider.ts | 515 +++ 19 files changed, 2115 insertions(+), 4035 deletions(-) create mode 100644 apps/sim/app/(auth)/components/sso-login-button.tsx create mode 100644 apps/sim/app/api/auth/sso/providers/route.ts create mode 100644 apps/sim/app/api/auth/sso/register/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/sso/sso.tsx delete mode 100644 bun.lock create mode 100644 packages/db/scripts/register-sso-provider.ts diff --git a/apps/sim/app/(auth)/components/sso-login-button.tsx b/apps/sim/app/(auth)/components/sso-login-button.tsx new file mode 100644 index 0000000000..5e42815d66 --- /dev/null +++ b/apps/sim/app/(auth)/components/sso-login-button.tsx @@ -0,0 +1,128 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { client } from '@/lib/auth-client' +import { env, isTruthy } from '@/lib/env' +import { createLogger } from '@/lib/logs/console/logger' +import { cn } from '@/lib/utils' +import { inter } from '@/app/fonts/inter' + +const logger = createLogger('SSOLoginButton') + +interface SSOLoginButtonProps { + callbackURL?: string + className?: string +} + +export function SSOLoginButton({ callbackURL, className }: SSOLoginButtonProps) { + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [showEmailInput, setShowEmailInput] = useState(false) + + // Don't render if SSO is not enabled + if (!isTruthy(env.NEXT_PUBLIC_SSO_ENABLED)) { + return null + } + + const handleSSOSignIn = async () => { + if (!email) { + setError('Email is required for SSO sign-in') + return + } + + setIsLoading(true) + setError(null) + + try { + // Better-auth SSO sign-in according to documentation + // Use email-based domain matching + await client.signIn.sso({ + email: email, + callbackURL: callbackURL || '/workspace', + }) + } catch (err) { + logger.error('SSO sign-in failed', { error: err, email }) + + // Parse the error to show a better message + let errorMessage = 'SSO sign-in failed' + if (err instanceof Error) { + if (err.message.includes('NO_PROVIDER_FOUND')) { + errorMessage = 'SSO provider not found. Please check your configuration.' + } else if (err.message.includes('INVALID_EMAIL_DOMAIN')) { + errorMessage = 'Email domain not configured for SSO. Please contact your administrator.' + } else { + errorMessage = err.message + } + } + + setError(errorMessage) + setIsLoading(false) + } + } + + if (!showEmailInput) { + return ( + + ) + } + + return ( +
+
+ + setEmail(e.target.value)} + className='rounded-[10px] shadow-sm' + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSSOSignIn() + } + }} + /> + {error &&

{error}

} +
+ +
+ + +
+
+ ) +} diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index d30b03b634..dceb4d36a1 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -16,9 +16,11 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { client } from '@/lib/auth-client' import { quickValidateEmail } from '@/lib/email/validation' +import { env, isTruthy } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' +import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' import { inter } from '@/app/fonts/inter' import { soehne } from '@/app/fonts/soehne/soehne' @@ -376,106 +378,121 @@ export default function LoginPage({

-
-
-
-
- -
- 0 && - 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' - )} - /> - {showEmailValidationError && emailErrors.length > 0 && ( -
- {emailErrors.map((error, index) => ( -

{error}

- ))} + {/* SSO Login Button */} + {isTruthy(env.NEXT_PUBLIC_SSO_ENABLED) && ( +
+ +
+ )} + + {/* Email/Password Form - only show if enabled */} + {isTruthy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && ( + +
+
+
+
- )} -
-
-
- - -
-
0 && + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showEmailValidationError && + emailErrors.length > 0 && 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' )} /> - + {showEmailValidationError && emailErrors.length > 0 && ( +
+ {emailErrors.map((error, index) => ( +

{error}

+ ))} +
+ )}
- {showValidationError && passwordErrors.length > 0 && ( -
- {passwordErrors.map((error, index) => ( -

{error}

- ))} +
+
+ +
- )} +
+ 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + +
+ {showValidationError && passwordErrors.length > 0 && ( +
+ {passwordErrors.map((error, index) => ( +

{error}

+ ))} +
+ )} +
-
- - - - {(githubAvailable || googleAvailable) && ( -
-
-
-
-
- Or continue with -
-
+ + )} + {/* Divider - show when we have multiple auth methods */} + {(githubAvailable || googleAvailable) && + (isTruthy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) || + isTruthy(env.NEXT_PUBLIC_SSO_ENABLED)) && ( +
+
+
+
+
+ + Or continue with + +
+
+ )} + -
- Don't have an account? - - Sign up - -
+ {/* Only show signup link if email/password signup is enabled */} + {isTruthy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && ( +
+ Don't have an account? + + Sign up + +
+ )}
-
-
-
-
- -
- 0 && - 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' - )} - /> - {showNameValidationError && nameErrors.length > 0 && ( -
- {nameErrors.map((error, index) => ( -

{error}

- ))} + {/* SSO Login Button */} + {isTruthy(env.NEXT_PUBLIC_SSO_ENABLED) && ( +
+ +
+ )} + + {/* Email/Password Form - only show if enabled */} + {isTruthy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && ( + +
+
+
+
- )} -
-
-
- -
- 0)) && - 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showNameValidationError && nameErrors.length > 0 && ( +
+ {nameErrors.map((error, index) => ( +

{error}

+ ))} +
)} - /> - {showEmailValidationError && emailErrors.length > 0 && ( -
- {emailErrors.map((error, index) => ( -

{error}

- ))} -
- )} - {emailError && !showEmailValidationError && ( -
-

{emailError}

-
- )} -
-
-
-
-
+
+
+ +
0 && + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + (emailError || (showEmailValidationError && emailErrors.length > 0)) && 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' )} /> - + {showEmailValidationError && emailErrors.length > 0 && ( +
+ {emailErrors.map((error, index) => ( +

{error}

+ ))} +
+ )} + {emailError && !showEmailValidationError && ( +
+

{emailError}

+
+ )}
- {showValidationError && passwordErrors.length > 0 && ( -
- {passwordErrors.map((error, index) => ( -

{error}

- ))} +
+
+
- )} +
+ 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + +
+ {showValidationError && passwordErrors.length > 0 && ( +
+ {passwordErrors.map((error, index) => ( +

{error}

+ ))} +
+ )} +
-
- - - - {(githubAvailable || googleAvailable) && ( -
-
-
-
-
- Or continue with -
-
+ + )} + {/* Divider - show when we have multiple auth methods */} + {(githubAvailable || googleAvailable) && + (isTruthy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) || + isTruthy(env.NEXT_PUBLIC_SSO_ENABLED)) && ( +
+
+
+
+
+ + Or continue with + +
+
+ )} + -
- Already have an account? - - Sign in - -
+ {/* Only show login link if email/password signup is enabled */} + {isTruthy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && ( +
+ Already have an account? + + Sign in + +
+ )}
({ + ...provider, + providerType: provider.oidcConfig ? 'oidc' : ('saml' as 'oidc' | 'saml'), + })) + } else { + // Public access - get all providers for login use + const results = await db + .select({ + id: ssoProvider.id, + providerId: ssoProvider.providerId, + domain: ssoProvider.domain, + oidcConfig: ssoProvider.oidcConfig, + samlConfig: ssoProvider.samlConfig, + }) + .from(ssoProvider) + + providers = results.map((provider) => ({ + id: provider.id, + providerId: provider.providerId, + domain: provider.domain, + providerType: provider.oidcConfig ? 'oidc' : ('saml' as 'oidc' | 'saml'), + })) + } + + logger.info('Fetched SSO providers', { + userId: session?.user?.id, + authenticated: !!session?.user?.id, + providerCount: providers.length, + }) + + return NextResponse.json({ providers }) + } catch (error) { + logger.error('Failed to fetch SSO providers', { error }) + return NextResponse.json({ error: 'Failed to fetch SSO providers' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/auth/sso/register/route.ts b/apps/sim/app/api/auth/sso/register/route.ts new file mode 100644 index 0000000000..c974a2d732 --- /dev/null +++ b/apps/sim/app/api/auth/sso/register/route.ts @@ -0,0 +1,195 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { auth } from '@/lib/auth' +import { env } from '@/lib/env' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('SSO-Register') + +export async function POST(request: NextRequest) { + try { + if (!env.SSO_ENABLED) { + return NextResponse.json({ error: 'SSO is not enabled' }, { status: 400 }) + } + + const body = await request.json() + const { + providerId, + issuer, + domain, + providerType = 'oidc', // Default to OIDC + // OIDC specific fields + clientId, + clientSecret, + scopes = ['openid', 'profile', 'email'], + pkce = true, + // SAML specific fields + entryPoint, + cert, + callbackUrl, + audience, + wantAssertionsSigned, + signatureAlgorithm, + digestAlgorithm, + identifierFormat, + // Mapping configuration + mapping = { + id: 'sub', + email: 'email', + name: 'name', + image: 'picture', + }, + } = body + + // Validate required fields + if (!providerId || !issuer || !domain) { + return NextResponse.json( + { error: 'Missing required fields: providerId, issuer, domain' }, + { status: 400 } + ) + } + + // Validate provider-specific required fields + if (providerType === 'oidc') { + if (!clientId || !clientSecret) { + return NextResponse.json( + { error: 'Missing required OIDC fields: clientId, clientSecret' }, + { status: 400 } + ) + } + } else if (providerType === 'saml') { + if (!entryPoint || !cert) { + return NextResponse.json( + { error: 'Missing required SAML fields: entryPoint, cert' }, + { status: 400 } + ) + } + } + + const headers: Record = {} + request.headers.forEach((value, key) => { + headers[key] = value + }) + + // Build configuration based on provider type + const providerConfig: any = { + providerId, + issuer, + domain, + mapping, + } + + if (providerType === 'oidc') { + // Build OIDC configuration with manual endpoints for Okta + const oidcConfig: any = { + clientId, + clientSecret, + scopes: + typeof scopes === 'string' + ? scopes + .split(',') + .map((s: string) => s.trim()) + .filter((s: string) => s !== 'offline_access') + : (scopes || ['openid', 'profile', 'email']).filter( + (s: string) => s !== 'offline_access' + ), + 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` + } + + logger.info('Using manual OIDC endpoints for provider', { + providerId, + provider: issuer.includes('okta.com') + ? 'Okta' + : issuer.includes('auth0.com') + ? 'Auth0' + : 'Generic', + authEndpoint: oidcConfig.authorizationEndpoint, + }) + } + + providerConfig.oidcConfig = oidcConfig + } else if (providerType === 'saml') { + // Build SAML configuration + const samlConfig: any = { + entryPoint, + cert, + } + + // Add optional SAML fields + if (callbackUrl) samlConfig.callbackUrl = callbackUrl + if (audience) samlConfig.audience = audience + if (wantAssertionsSigned !== undefined) samlConfig.wantAssertionsSigned = wantAssertionsSigned + if (signatureAlgorithm) samlConfig.signatureAlgorithm = signatureAlgorithm + if (digestAlgorithm) samlConfig.digestAlgorithm = digestAlgorithm + if (identifierFormat) samlConfig.identifierFormat = identifierFormat + + providerConfig.samlConfig = samlConfig + } + + const registration = await auth.api.registerSSOProvider({ + body: providerConfig, + headers, + }) + + logger.info('SSO provider registered successfully', { + providerId, + providerType, + domain, + }) + + return NextResponse.json({ + success: true, + providerId: registration.providerId, + providerType, + message: `${providerType.toUpperCase()} provider registered successfully`, + }) + } catch (error) { + logger.error('Failed to register SSO provider', { + error, + errorMessage: error instanceof Error ? error.message : 'Unknown error', + errorStack: error instanceof Error ? error.stack : undefined, + errorDetails: JSON.stringify(error), + }) + + return NextResponse.json( + { + error: 'Failed to register SSO provider', + details: error instanceof Error ? error.message : 'Unknown error', + fullError: JSON.stringify(error), + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts index 139d1af73a..1bd2b3d26f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts @@ -7,5 +7,6 @@ export { General } from './general/general' export { MCP } from './mcp/mcp' export { Privacy } from './privacy/privacy' export { SettingsNavigation } from './settings-navigation/settings-navigation' +export { SSO } from './sso/sso' export { Subscription } from './subscription/subscription' export { TeamManagement } from './team-management/team-management' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx index ebeb2ddec2..b8257baa64 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx @@ -11,9 +11,11 @@ import { Users, Waypoints, } from 'lucide-react' +import { useSession } from '@/lib/auth-client' import { getEnv, isTruthy } from '@/lib/env' import { isHosted } from '@/lib/environment' import { cn } from '@/lib/utils' +import { useOrganizationStore } from '@/stores/organization' const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) @@ -28,6 +30,7 @@ interface SettingsNavigationProps { | 'apikeys' | 'subscription' | 'team' + | 'sso' | 'privacy' | 'copilot' | 'mcp' @@ -44,6 +47,7 @@ type NavigationItem = { | 'apikeys' | 'subscription' | 'team' + | 'sso' | 'copilot' | 'privacy' | 'mcp' @@ -51,6 +55,8 @@ type NavigationItem = { icon: React.ComponentType<{ className?: string }> hideWhenBillingDisabled?: boolean requiresTeam?: boolean + requiresEnterprise?: boolean + requiresOwner?: boolean } const allNavigationItems: NavigationItem[] = [ @@ -107,6 +113,14 @@ const allNavigationItems: NavigationItem[] = [ hideWhenBillingDisabled: true, requiresTeam: true, }, + { + id: 'sso', + label: 'Single Sign-On', + icon: Shield, + requiresTeam: true, + requiresEnterprise: true, + requiresOwner: true, + }, ] export function SettingsNavigation({ @@ -114,6 +128,12 @@ export function SettingsNavigation({ onSectionChange, hasOrganization, }: SettingsNavigationProps) { + const { data: session } = useSession() + const { hasEnterprisePlan, getUserRole } = useOrganizationStore() + const userEmail = session?.user?.email + const userRole = getUserRole(userEmail) + const isOwner = userRole === 'owner' + const navigationItems = allNavigationItems.filter((item) => { if (item.id === 'copilot' && !isHosted) { return false @@ -122,11 +142,18 @@ export function SettingsNavigation({ return false } - // Hide team tab if user doesn't have an active organization if (item.requiresTeam && !hasOrganization) { return false } + if (item.requiresEnterprise && !hasEnterprisePlan) { + return false + } + + if (item.requiresOwner && !isOwner) { + return false + } + return true }) @@ -169,7 +196,7 @@ export function SettingsNavigation({ ))}
- {/* Homepage link - Only show in hosted environments */} + {/* Homepage link */} {isHosted && (
+
+
+ +
+
+
+ Issuer URL +

+ {provider.issuer} +

+
+
+ Provider ID +

{provider.providerId}

+
+
+ +
+ + Callback URL + +
+ (e.target as HTMLInputElement).select()} + /> + +
+
+
+
+ ))} +
+ ) : ( + // SSO Configuration Form + <> + {hasProviders && ( +
+ +
+ )} +
+ {/* Provider Type Selection */} +
+ +
+ + +
+

+ {formData.providerType === 'oidc' + ? 'OpenID Connect (Okta, Azure AD, Auth0, etc.)' + : 'Security Assertion Markup Language (ADFS, Shibboleth, etc.)'} +

+
+ +
+ + handleInputChange('providerId', e.target.value)} + className={cn( + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.providerId.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showErrors && errors.providerId.length > 0 && ( +
+

{errors.providerId.join(' ')}

+
+ )} +

+ A unique identifier for your SSO provider +

+
+ +
+ + handleInputChange('issuerUrl', e.target.value)} + className={cn( + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.issuerUrl.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showErrors && errors.issuerUrl.length > 0 && ( +
+

{errors.issuerUrl.join(' ')}

+
+ )} +

+ {formData.providerType === 'oidc' + ? 'The OIDC issuer URL from your identity provider' + : 'The base URL or entity ID of your SAML identity provider'} +

+
+ +
+ + handleInputChange('domain', e.target.value)} + className={cn( + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.domain.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showErrors && errors.domain.length > 0 && ( +
+

{errors.domain.join(' ')}

+
+ )} +

Your identity provider domain

+
+ + {/* Provider-specific fields */} + {formData.providerType === 'oidc' ? ( + <> +
+ + handleInputChange('clientId', e.target.value)} + className={cn( + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.clientId.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showErrors && errors.clientId.length > 0 && ( +
+

{errors.clientId.join(' ')}

+
+ )} +

+ The application client ID from your identity provider +

+
+ +
+ +
+ handleInputChange('clientSecret', e.target.value)} + onFocus={() => setShowClientSecret(true)} + onBlurCapture={() => setShowClientSecret(false)} + className={cn( + 'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.clientSecret.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + +
+ {showErrors && errors.clientSecret.length > 0 && ( +
+

{errors.clientSecret.join(' ')}

+
+ )} +

+ The application client secret from your identity provider +

+
+ +
+ + handleInputChange('scopes', e.target.value)} + className={cn( + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.scopes.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showErrors && errors.scopes.length > 0 && ( +
+

{errors.scopes.join(' ')}

+
+ )} +

+ Comma-separated list of OIDC scopes to request +

+
+ + ) : ( + <> +
+ + handleInputChange('entryPoint', e.target.value)} + className={cn( + 'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100', + showErrors && + errors.entryPoint.length > 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + /> + {showErrors && errors.entryPoint.length > 0 && ( +
+

{errors.entryPoint.join(' ')}

+
+ )} +

+ The SAML SSO login URL from your identity provider +

+
+ +
+ +