import { zodResolver } from '@hookform/resolvers/zod' import { Trash } from 'lucide-react' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import z from 'zod' import { AttributeMapping } from './AttributeMapping' import { JoinOrganizationOnSignup } from './JoinOrganizationOnSignup' import { SSODomains } from './SSODomains' import { SSOMetadata } from './SSOMetadata' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import AlertError from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' import { UpgradeToPro } from '@/components/ui/UpgradeToPro' import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query' import { useSSOConfigCreateMutation } from '@/data/sso/sso-config-create-mutation' import { useSSOConfigDeleteMutation } from '@/data/sso/sso-config-delete-mutation' import { useOrgSSOConfigQuery } from '@/data/sso/sso-config-query' import { useSSOConfigUpdateMutation } from '@/data/sso/sso-config-update-mutation' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' import { DOCS_URL } from '@/lib/constants' const FormSchema = z .object({ enabled: z.boolean(), enableSpInitiated: z.boolean(), domains: z.array( z.object({ value: z.string().trim(), }) ), metadataXmlUrl: z.string().trim().optional(), metadataXmlFile: z.string().trim().optional(), emailMapping: z.array(z.object({ value: z.string().trim().min(1, 'This field is required') })), userNameMapping: z.array(z.object({ value: z.string().trim() })), firstNameMapping: z.array(z.object({ value: z.string().trim() })), lastNameMapping: z.array(z.object({ value: z.string().trim() })), joinOrgOnSignup: z.boolean(), roleOnJoin: z.string().optional(), }) .superRefine((data, ctx) => { if (!data.enableSpInitiated) return const hasValidDomain = data.domains?.some((d) => d.value && d.value.trim().length > 0) if (!hasValidDomain) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'At least one domain is required when SP-initiated login is enabled', path: ['domains'], }) } data.domains?.forEach((d, idx) => { if (!d.value || d.value.trim().length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide a domain', path: ['domains', idx, 'value'], }) } }) }) // set the error on both fields .refine((data) => data.metadataXmlUrl || data.metadataXmlFile, { message: 'Please provide either a metadata XML URL or upload a metadata XML file', path: ['metadataXmlUrl'], }) .refine((data) => data.metadataXmlUrl || data.metadataXmlFile, { message: 'Please provide either a metadata XML URL or upload a metadata XML file', path: ['metadataXmlFile'], }) export type SSOConfigFormSchema = z.infer const defaultValues = { enabled: false, enableSpInitiated: false, domains: [{ value: '' }], metadataXmlUrl: '', metadataXmlFile: '', emailMapping: [{ value: '' }], userNameMapping: [{ value: '' }], firstNameMapping: [{ value: '' }], lastNameMapping: [{ value: '' }], joinOrgOnSignup: false, roleOnJoin: 'Developer', } export const SSOConfig = () => { const FORM_ID = 'sso-config-form' const { data: organization } = useSelectedOrganizationQuery() const { hasAccess: hasAccessToSso, isLoading: isLoadingEntitlement } = useCheckEntitlements('auth.platform.sso') const { data: ssoConfig, isPending: isLoadingSSOConfig, isSuccess, isError, error: configError, } = useOrgSSOConfigQuery({ orgSlug: organization?.slug }, { enabled: !!organization }) const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug }) const ssoMemberCount = members.filter((m) => m.is_sso_user === true).length const isSSOProviderNotFound = ssoConfig === null const form = useForm({ resolver: zodResolver(FormSchema as any), defaultValues, }) const isSSOEnabled = form.watch('enabled') const enableSpInitiated = form.watch('enableSpInitiated') const { mutate: createSSOConfig, isPending: isCreating } = useSSOConfigCreateMutation({ onSuccess: () => { toast.success('Successfully created SSO configuration') // Reset form to current values to mark as clean // This allows useEffect to reset with fresh data when query refetches form.reset(form.getValues()) }, }) const { mutate: updateSSOConfig, isPending: isUpdating } = useSSOConfigUpdateMutation({ onSuccess: () => { toast.success('Successfully updated SSO configuration') // Reset form to current values to mark as clean // This allows useEffect to reset with fresh data when query refetches form.reset(form.getValues()) }, }) const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false) const { mutate: deleteSSOConfig, isPending: isDeleting } = useSSOConfigDeleteMutation({ onSuccess: () => { toast.success('Successfully deleted SSO configuration') setIsDeleteModalVisible(false) form.reset(defaultValues) }, }) const onSubmit: SubmitHandler = (values) => { const roleOnJoin = (values.roleOnJoin || 'Developer') as | 'Administrator' | 'Developer' | 'Owner' | 'Read-only' | undefined const payload = { slug: organization!.slug, config: { enabled: values.enabled, // Send empty array if SP-initiated is disabled domains: values.enableSpInitiated ? values.domains.map((d) => d.value).filter(Boolean) : [], metadata_xml_file: values.metadataXmlFile!, metadata_xml_url: values.metadataXmlUrl!, email_mapping: values.emailMapping.map((item) => item.value).filter(Boolean), first_name_mapping: values.firstNameMapping.map((item) => item.value).filter(Boolean), last_name_mapping: values.lastNameMapping.map((item) => item.value).filter(Boolean), user_name_mapping: values.userNameMapping.map((item) => item.value).filter(Boolean), join_org_on_signup_enabled: values.joinOrgOnSignup, join_org_on_signup_role: roleOnJoin, }, } if (!!ssoConfig) { updateSSOConfig(payload) } else { createSSOConfig(payload) } } const onDeleteSSOConfig = () => { if (!organization?.slug) return deleteSSOConfig({ slug: organization.slug }) } const syncFormFromConfig = useStaticEffectEvent(() => { if (!organization?.slug) return // Only reset form if it's not dirty (user hasn't made changes) if (ssoConfig && !form.formState.isDirty) { form.reset({ enabled: ssoConfig.enabled, // Infer SP-initiated from domains presence enableSpInitiated: ssoConfig.domains && ssoConfig.domains.length > 0, domains: ssoConfig.domains?.map((domain) => ({ value: domain })) || [], metadataXmlUrl: ssoConfig.metadata_xml_url, metadataXmlFile: ssoConfig.metadata_xml_file, emailMapping: ssoConfig.email_mapping.map((email) => ({ value: email })), userNameMapping: ssoConfig.user_name_mapping?.map((userName) => ({ value: userName })) || [], firstNameMapping: ssoConfig.first_name_mapping?.map((firstName) => ({ value: firstName })) || [], lastNameMapping: ssoConfig.last_name_mapping?.map((lastName) => ({ value: lastName })) || [], joinOrgOnSignup: ssoConfig.join_org_on_signup_enabled, roleOnJoin: ssoConfig.join_org_on_signup_role, }) } }) useEffect(() => { syncFormFromConfig() }, [ssoConfig, organization?.slug, syncFormFromConfig]) // Automatically add an empty domain field when SP-initiated is enabled const ensureDomainField = useStaticEffectEvent(() => { const currentDomains = form.getValues('domains') if (enableSpInitiated && (!currentDomains || currentDomains.length === 0)) { form.setValue('domains', [{ value: '' }], { shouldValidate: false }) } }) useEffect(() => { ensureDomainField() }, [enableSpInitiated, ensureDomainField]) return ( {isLoadingEntitlement || (hasAccessToSso && isLoadingSSOConfig) ? ( ) : isError && !isSSOProviderNotFound ? ( ) : !hasAccessToSso ? ( ) : isSuccess || isSSOProviderNotFound ? ( <>
( Enable and configure SSO for your organization. Learn more about SSO{' '} here . } > )} /> {isSSOEnabled && ( <> ( )} /> {form.watch('enableSpInitiated') && (
SP-initiated (Service Provider): Users start at supabase.com, enter their email address, and are redirected to your identity provider (Okta, Azure AD, etc.) for authentication. Requires configuring email domains.
IdP-initiated (Identity Provider): Users click an app tile or bookmark in your identity provider dashboard and are directly authenticated into Briven. Works automatically without domain configuration.

Most enterprises use IdP-initiated flow for its simplicity. Enable SP-initiated only if you need users to start at supabase.com.{' '} Learn more about SSO flows .

)}
{form.watch('enableSpInitiated') && ( )} )}
{!!ssoConfig && ( )}
{form.formState.isDirty && ( )}
setIsDeleteModalVisible(false)} >

You are about to delete the SSO provider {ssoConfig?.domains?.[0] && ( <> {' '} for{' '} {ssoConfig.domains[0]} )} .

{ssoMemberCount > 0 && (

{ssoMemberCount} organization member{ssoMemberCount !== 1 ? 's' : ''} {' '} who authenticate via SSO will be{' '} permanently removed from this organization.

)}

This action will:

  • Disable SSO authentication for this organization
  • Remove all members who signed up using SSO
  • Prevent future SSO-based sign-ins

This action cannot be undone. {' '} Members will need to be re-invited if you wish to restore their access.

) : null}
) }