Skip to content

Commit 1a0203e

Browse files
committed
feat(sso): expose the standard enterprise IdP options in the setup form
Rounds out the form with the options Better Auth already accepts but the UI hid, so a non-standard IdP no longer dead-ends at a field that cannot be set. SAML gains signature algorithm, digest algorithm and NameID format. Only SHA-256 and stronger are offered: Better Auth warns on SHA-1 as deprecated and rejects anything outside its secure set, so weaker choices would only produce failed saves. SAML also surfaces the SP Entity ID beside the ACS URL. IdP admins are usually handed a vendor metadata document; Sim does not publish one, and these are the two values it would carry. OIDC gains authorization, token and JWKS endpoint overrides for providers whose discovery document is incomplete or unreachable. Discovery still fills them in when they are left blank. All of these load from the stored config when editing, so re-saving a provider cannot quietly drop them.
1 parent 016dbba commit 1a0203e

1 file changed

Lines changed: 188 additions & 1 deletion

File tree

apps/sim/ee/sso/components/sso-settings.tsx

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ const SAML_DEFAULT_MAPPING = {
6060
name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
6161
} as const
6262

63+
const SAML_SIGNATURE_ALGORITHMS = [
64+
{ label: 'Provider default', value: '' },
65+
{ label: 'RSA-SHA256', value: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' },
66+
{ label: 'RSA-SHA384', value: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' },
67+
{ label: 'RSA-SHA512', value: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' },
68+
] as const
69+
70+
const SAML_DIGEST_ALGORITHMS = [
71+
{ label: 'Provider default', value: '' },
72+
{ label: 'SHA-256', value: 'http://www.w3.org/2001/04/xmlenc#sha256' },
73+
{ label: 'SHA-384', value: 'http://www.w3.org/2001/04/xmldsig-more#sha384' },
74+
{ label: 'SHA-512', value: 'http://www.w3.org/2001/04/xmlenc#sha512' },
75+
] as const
76+
77+
const SAML_NAMEID_FORMATS = [
78+
{ label: 'Provider default', value: '' },
79+
{
80+
label: 'Email address',
81+
value: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
82+
},
83+
{ label: 'Persistent', value: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' },
84+
{ label: 'Transient', value: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' },
85+
{ label: 'Unspecified', value: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' },
86+
] as const
87+
6388
const DEFAULT_FORM_DATA = {
6489
providerType: 'oidc' as 'oidc' | 'saml',
6590
providerId: '',
@@ -77,6 +102,12 @@ const DEFAULT_FORM_DATA = {
77102
mapId: '',
78103
mapEmail: '',
79104
mapName: '',
105+
signatureAlgorithm: '',
106+
digestAlgorithm: '',
107+
identifierFormat: '',
108+
authorizationEndpoint: '',
109+
tokenEndpoint: '',
110+
jwksEndpoint: '',
80111
}
81112

82113
const DEFAULT_ERRORS = {
@@ -295,6 +326,15 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
295326
clientId: formData.clientId,
296327
clientSecret: formData.clientSecret,
297328
scopes: formData.scopes.split(',').map((s) => s.trim()),
329+
...(formData.authorizationEndpoint.trim()
330+
? { authorizationEndpoint: formData.authorizationEndpoint.trim() }
331+
: {}),
332+
...(formData.tokenEndpoint.trim()
333+
? { tokenEndpoint: formData.tokenEndpoint.trim() }
334+
: {}),
335+
...(formData.jwksEndpoint.trim()
336+
? { jwksEndpoint: formData.jwksEndpoint.trim() }
337+
: {}),
298338
}
299339
: {
300340
providerType: 'saml',
@@ -313,6 +353,11 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
313353
...(formData.callbackUrl ? { callbackUrl: formData.callbackUrl } : {}),
314354
...(formData.audience ? { audience: formData.audience } : {}),
315355
...(formData.idpMetadata ? { idpMetadata: formData.idpMetadata } : {}),
356+
...(formData.signatureAlgorithm
357+
? { signatureAlgorithm: formData.signatureAlgorithm }
358+
: {}),
359+
...(formData.digestAlgorithm ? { digestAlgorithm: formData.digestAlgorithm } : {}),
360+
...(formData.identifierFormat ? { identifierFormat: formData.identifierFormat } : {}),
316361
}
317362

318363
await configureSSOMutation.mutateAsync(requestBody)
@@ -364,13 +409,22 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
364409
// that actually differs — otherwise editing would rewrite a default as an
365410
// explicit override, and a stored custom mapping must never silently reset.
366411
let mapping: { id?: string; email?: string; name?: string } = {}
412+
let signatureAlgorithm = ''
413+
let digestAlgorithm = ''
414+
let identifierFormat = ''
415+
let authorizationEndpoint = ''
416+
let tokenEndpoint = ''
417+
let jwksEndpoint = ''
367418

368419
if (existingProvider.providerType === 'oidc' && existingProvider.oidcConfig) {
369420
const config = JSON.parse(existingProvider.oidcConfig)
370421
clientId = config.clientId || ''
371422
clientSecret = config.clientSecret || ''
372423
scopes = config.scopes?.join(',') || 'openid,profile,email'
373424
mapping = config.mapping ?? {}
425+
authorizationEndpoint = config.authorizationEndpoint || ''
426+
tokenEndpoint = config.tokenEndpoint || ''
427+
jwksEndpoint = config.jwksEndpoint || ''
374428
} else if (existingProvider.providerType === 'saml' && existingProvider.samlConfig) {
375429
const config = JSON.parse(existingProvider.samlConfig)
376430
entryPoint = config.entryPoint || ''
@@ -380,6 +434,9 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
380434
wantAssertionsSigned = config.wantAssertionsSigned ?? true
381435
idpMetadata = config.idpMetadata?.metadata || config.idpMetadata || ''
382436
mapping = config.mapping ?? {}
437+
signatureAlgorithm = config.signatureAlgorithm || ''
438+
digestAlgorithm = config.digestAlgorithm || ''
439+
identifierFormat = config.identifierFormat || ''
383440
}
384441

385442
const defaults =
@@ -404,6 +461,12 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
404461
mapId: overrideOf(mapping.id, defaults.id),
405462
mapEmail: overrideOf(mapping.email, defaults.email),
406463
mapName: overrideOf(mapping.name, defaults.name),
464+
signatureAlgorithm,
465+
digestAlgorithm,
466+
identifierFormat,
467+
authorizationEndpoint,
468+
tokenEndpoint,
469+
jwksEndpoint,
407470
}
408471
setFormData(snapshot)
409472
setOriginalFormData(snapshot)
@@ -667,6 +730,71 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
667730
/>
668731
</SettingRow>
669732

733+
<div className='flex flex-col gap-2'>
734+
<Button
735+
type='button'
736+
variant='ghost'
737+
onClick={() => setShowAdvanced((v) => !v)}
738+
className='w-fit gap-1.5 px-0 text-[var(--text-muted)] hover:bg-transparent hover:text-[var(--text-primary)]'
739+
>
740+
<ChevronDown
741+
className={cn(
742+
'size-[14px] transition-transform',
743+
showAdvanced && 'rotate-180'
744+
)}
745+
/>
746+
Advanced Options
747+
</Button>
748+
749+
<Expandable expanded={showAdvanced}>
750+
<ExpandableContent>
751+
<div className='flex flex-col gap-4.5 pt-2'>
752+
<SettingRow label='Authorization endpoint' optional>
753+
<ChipInput
754+
type='url'
755+
placeholder='Discovered from the issuer'
756+
value={formData.authorizationEndpoint}
757+
autoComplete='off'
758+
autoCapitalize='none'
759+
spellCheck={false}
760+
onChange={(e) =>
761+
handleInputChange('authorizationEndpoint', e.target.value)
762+
}
763+
/>
764+
</SettingRow>
765+
766+
<SettingRow label='Token endpoint' optional>
767+
<ChipInput
768+
type='url'
769+
placeholder='Discovered from the issuer'
770+
value={formData.tokenEndpoint}
771+
autoComplete='off'
772+
autoCapitalize='none'
773+
spellCheck={false}
774+
onChange={(e) => handleInputChange('tokenEndpoint', e.target.value)}
775+
/>
776+
</SettingRow>
777+
778+
<SettingRow label='JWKS endpoint' optional>
779+
<ChipInput
780+
type='url'
781+
placeholder='Discovered from the issuer'
782+
value={formData.jwksEndpoint}
783+
autoComplete='off'
784+
autoCapitalize='none'
785+
spellCheck={false}
786+
onChange={(e) => handleInputChange('jwksEndpoint', e.target.value)}
787+
/>
788+
<p className='text-[var(--text-muted)] text-small'>
789+
Sim reads these from the issuer's discovery document. Set them only if
790+
your provider does not publish one.
791+
</p>
792+
</SettingRow>
793+
</div>
794+
</ExpandableContent>
795+
</Expandable>
796+
</div>
797+
670798
<SettingRow
671799
label='Scopes'
672800
error={
@@ -782,6 +910,51 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
782910
/>
783911
</SettingRow>
784912

913+
<SettingRow label='Signature algorithm' optional>
914+
<ChipSelect
915+
align='start'
916+
value={formData.signatureAlgorithm}
917+
onChange={(value: string) =>
918+
handleInputChange('signatureAlgorithm', value)
919+
}
920+
options={SAML_SIGNATURE_ALGORITHMS.map((a) => ({
921+
label: a.label,
922+
value: a.value,
923+
}))}
924+
placeholder='Provider default'
925+
/>
926+
</SettingRow>
927+
928+
<SettingRow label='Digest algorithm' optional>
929+
<ChipSelect
930+
align='start'
931+
value={formData.digestAlgorithm}
932+
onChange={(value: string) =>
933+
handleInputChange('digestAlgorithm', value)
934+
}
935+
options={SAML_DIGEST_ALGORITHMS.map((a) => ({
936+
label: a.label,
937+
value: a.value,
938+
}))}
939+
placeholder='Provider default'
940+
/>
941+
</SettingRow>
942+
943+
<SettingRow label='NameID format' optional>
944+
<ChipSelect
945+
align='start'
946+
value={formData.identifierFormat}
947+
onChange={(value: string) =>
948+
handleInputChange('identifierFormat', value)
949+
}
950+
options={SAML_NAMEID_FORMATS.map((a) => ({
951+
label: a.label,
952+
value: a.value,
953+
}))}
954+
placeholder='Provider default'
955+
/>
956+
</SettingRow>
957+
785958
<SettingRow label='IDP Metadata XML' optional>
786959
<ChipTextarea
787960
placeholder='Paste IDP metadata XML here'
@@ -801,13 +974,27 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
801974
</>
802975
)}
803976

804-
<SettingRow label='Callback URL'>
977+
<SettingRow label={isSaml ? 'ACS URL (Reply URL)' : 'Callback URL'}>
805978
<ChipCopyInput value={callbackUrl} copyLabel='Copy callback URL' />
806979
<p className='text-[var(--text-muted)] text-small'>
807980
Configure this in your identity provider
808981
</p>
809982
</SettingRow>
810983

984+
{/* SAML IdP admins are typically handed vendor metadata; Sim does not
985+
publish a metadata document, so surface the two values that document
986+
would carry. Sim's SP entity ID is its base URL — the same value the
987+
register route embeds in the generated SP metadata. */}
988+
{isSaml && (
989+
<SettingRow label='SP Entity ID'>
990+
<ChipCopyInput value={getBaseUrl()} copyLabel='Copy entity ID' />
991+
<p className='text-[var(--text-muted)] text-small'>
992+
Sim's identifier in your IdP. With the ACS URL above, this is everything needed to
993+
add Sim as a service provider.
994+
</p>
995+
</SettingRow>
996+
)}
997+
811998
{/* Identity providers vary in which claim carries each value — Entra,
812999
for instance, can send the address as `upn` rather than `email`.
8131000
Leaving a field blank uses the protocol default shown as its

0 commit comments

Comments
 (0)