@@ -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+
6388const 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
82113const 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