diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index f85260e029..89577dd8f3 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -214,97 +214,182 @@ public class ApplicationProperties { @Setter @ToString public static class SAML2 { - private String provider; private Boolean enabled = false; private Boolean autoCreateUser = false; private Boolean blockRegistration = false; private String registrationId = "stirling"; - - @ToString.Exclude - @JsonProperty("idpMetadataUri") - private String idpMetadataUri; - - private String idpSingleLogoutUrl; - private String idpSingleLoginUrl; - @Deprecated(since = "2.2.1", forRemoval = true) - private String idpIssuer; // Legacy field name, use idpEntityId instead - private String idpEntityId; // IdP Entity ID (preferred field name) private Boolean enableSingleLogout = false; - /** - * Gets the IdP Entity ID, checking both idpEntityId (preferred) and idpIssuer (legacy). - */ - @JsonIgnore - public String getIdpEntityIdOrIssuer() { - if (idpEntityId != null && !idpEntityId.isBlank()) { - return idpEntityId; - } - return idpIssuer; - } + @ToString.Exclude private String metadataUri; - @JsonProperty("idpCert") + private Provider provider = new Provider(); + private SP sp = new SP(); + + // Legacy field mappings for backward compatibility + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore + private String idpMetadataUri; + + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore + private String idpSingleLogoutUrl; + + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore + private String idpSingleLoginUrl; + + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore + private String idpIssuer; + + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore + private String idpEntityId; + + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore private String idpCert; - @ToString.Exclude - @JsonProperty("privateKey") + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore private String privateKey; - @ToString.Exclude - @JsonProperty("spCert") + @Deprecated(since = "2.1.5", forRemoval = true) + @JsonIgnore private String spCert; - @JsonIgnore - public InputStream getIdpMetadataUri() throws IOException { - if (idpMetadataUri == null || idpMetadataUri.isBlank()) { - throw new IOException("security.saml2.idpMetadataUri is not configured"); + /** Migrate legacy flat properties to new nested structure on set. */ + public void setIdpMetadataUri(String value) { + this.idpMetadataUri = value; + if (value != null + && !value.isBlank() + && (metadataUri == null || metadataUri.isBlank())) { + this.metadataUri = value; } - if (idpMetadataUri.startsWith("classpath:")) { - return new ClassPathResource(idpMetadataUri.substring("classpath:".length())) + } + + public void setIdpSingleLoginUrl(String value) { + this.idpSingleLoginUrl = value; + if (value != null && !value.isBlank()) { + this.provider.setSingleLoginUrl(value); + } + } + + public void setIdpSingleLogoutUrl(String value) { + this.idpSingleLogoutUrl = value; + if (value != null && !value.isBlank()) { + this.provider.setSingleLogoutUrl(value); + } + } + + public void setIdpIssuer(String value) { + this.idpIssuer = value; + if (value != null && !value.isBlank()) { + this.provider.setEntityId(value); + } + } + + public void setIdpEntityId(String value) { + this.idpEntityId = value; + if (value != null && !value.isBlank()) { + this.provider.setEntityId(value); + } + } + + public void setIdpCert(String value) { + this.idpCert = value; + if (value != null && !value.isBlank()) { + this.provider.setCert(value); + } + } + + public void setPrivateKey(String value) { + this.privateKey = value; + if (value != null && !value.isBlank()) { + this.sp.setPrivateKey(value); + } + } + + public void setSpCert(String value) { + this.spCert = value; + if (value != null && !value.isBlank()) { + this.sp.setCert(value); + } + } + + @JsonIgnore + public InputStream getMetadataUriAsStream() throws IOException { + String uri = getEffectiveMetadataUri(); + if (uri == null || uri.isBlank()) { + throw new IOException("security.saml2.metadataUri is not configured"); + } + if (uri.startsWith("classpath:")) { + return new ClassPathResource(uri.substring("classpath:".length())) .getInputStream(); } try { - URI uri = new URI(idpMetadataUri); - URL url = uri.toURL(); + URI parsedUri = new URI(uri); + URL url = parsedUri.toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); return connection.getInputStream(); } catch (URISyntaxException e) { - throw new IOException("Invalid URI format: " + idpMetadataUri, e); + throw new IOException("Invalid URI format: " + uri, e); } } @JsonIgnore - public String getIdpMetadataUriLocation() { - return idpMetadataUri; + public String getEffectiveMetadataUri() { + if (metadataUri != null && !metadataUri.isBlank()) { + return metadataUri; + } + return idpMetadataUri; // Legacy fallback } - @JsonIgnore - public Resource getSpCert() { - if (spCert == null) return null; - if (spCert.startsWith("classpath:")) { - return new ClassPathResource(spCert.substring("classpath:".length())); - } else { - return new FileSystemResource(spCert); + /** IdP configuration - manual fallback when metadata is unavailable. */ + @Data + public static class Provider { + private String name = ""; // Display name only + private String singleLoginUrl; + private String singleLogoutUrl; + private String entityId; + @ToString.Exclude private String cert; + + @JsonIgnore + public Resource getCertResource() { + if (cert == null) return null; + if (cert.startsWith("classpath:")) { + return new ClassPathResource(cert.substring("classpath:".length())); + } else { + return new FileSystemResource(cert); + } } } - @JsonIgnore - public Resource getIdpCert() { - if (idpCert == null) return null; - if (idpCert.startsWith("classpath:")) { - return new ClassPathResource(idpCert.substring("classpath:".length())); - } else { - return new FileSystemResource(idpCert); - } - } + /** Service Provider (SP) credentials for signing SAML requests. */ + @Data + public static class SP { + @ToString.Exclude private String privateKey; + @ToString.Exclude private String cert; - @JsonIgnore - public Resource getPrivateKey() { - if (privateKey == null) return null; - if (privateKey.startsWith("classpath:")) { - return new ClassPathResource(privateKey.substring("classpath:".length())); - } else { - return new FileSystemResource(privateKey); + @JsonIgnore + public Resource getPrivateKeyResource() { + if (privateKey == null) return null; + if (privateKey.startsWith("classpath:")) { + return new ClassPathResource(privateKey.substring("classpath:".length())); + } else { + return new FileSystemResource(privateKey); + } + } + + @JsonIgnore + public Resource getCertResource() { + if (cert == null) return null; + if (cert.startsWith("classpath:")) { + return new ClassPathResource(cert.substring("classpath:".length())); + } else { + return new FileSystemResource(cert); + } } } } diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index e26df0c94f..b3b4b25681 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -167,7 +167,7 @@ public class RequestUriUtils { || trimmedUri.startsWith("/api/v1/auth/refresh") || trimmedUri.startsWith("/logout") || trimmedUri.startsWith( - "/api/v1/proprietary/ui-data/login") // Login page config (SSO providers + + "/api/v1/proprietary/ui-data/login") // Login page config (SSO providers // enableLogin) || trimmedUri.startsWith( "/api/v1/ui-data/footer-info") // Public footer configuration diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 91643aa38a..dbcbaf592f 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -47,22 +47,23 @@ security: provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak' saml2: enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true) - provider: "" # The name of your Provider + enableSingleLogout: false # set to 'true' to enable Single Logout (SP-initiated SLO). Logs the user out from the IdP autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs - idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata - idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your IdP - enableSingleLogout: true # set to 'true' to enable Single Logout (SP-initiated SLO). Logs the user out from the IdP - idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your IdP - idpIssuer: "" # The entity ID of your IdP (Deprecated. Use idpEntityId instead) - idpEntityId: '' # The entity ID of your IdP - idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your IdP - privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair - spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair + metadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # RECOMMENDED: Your IdP's metadata URI. When provided, IdP config is auto-discovered. + provider: # IdP manual configuration - use if metadataUri is not available + name: "" # Display name for your IdP (optional) + singleLoginUrl: "" # SSO URL + singleLogoutUrl: "" # SLO URL + entityId: '' # IdP Entity ID + cert: classpath:okta.cert # IdP signing certificate + sp: # Service Provider (your app) credentials for signing SAML requests. Generated from your keypair + privateKey: classpath:saml-private-key.key # Your private key. + cert: classpath:saml-public-cert.crt # Your signing certificate. # IMPORTANT: For SAML setup, download your SP metadata from the BACKEND URL: http://localhost:8080/saml2/service-provider-metadata/{registrationId} # Do NOT use the frontend dev server URL (localhost:5173) as it will generate incorrect ACS URLs. Always use the backend URL (localhost:8080) for SAML configuration. - jwt: # This feature is currently under development and not yet fully supported. Do not use in production. + jwt: persistence: true # Set to 'true' to enable JWT key store enableKeyRotation: true # Set to 'true' to enable key pair rotation enableKeyCleanup: true # Set to 'true' to enable key pair cleanup diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java index 17d5b284c5..21f0b15c0e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java @@ -95,40 +95,38 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { private boolean handleSamlLogout( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - if (authentication instanceof Saml2Authentication samlAuthentication) { - CustomSaml2AuthenticatedPrincipal principal = - (CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal(); - String nameId = principal.nameId(); + if (securityProperties.getSaml2().getEnableSingleLogout()) { + log.info("SP-initiated SLO detected, logging out via IdP"); - if (securityProperties.getSaml2().getEnableSingleLogout()) { - log.info("SAML user {} logging out via IdP SLO (session-based)", nameId); + if (authentication instanceof Saml2Authentication samlAuthentication) { try { - samlLogoutHandler.onLogoutSuccess(request, response, authentication); + samlLogoutHandler.onLogoutSuccess(request, response, samlAuthentication); } catch (Exception e) { - log.error("SAML SLO failed, falling back to local logout", e); + log.error("SP-initiated SLO failed, falling back to local logout", e); getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); } + + return true; } else { - getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); + // Reconstruct Saml2Authentication from JWT claims for SLO + Optional reconstructedAuth = + reconstructSaml2AuthenticationFromJwt(request); + + if (reconstructedAuth.isPresent()) { + Saml2Authentication samlAuth = reconstructedAuth.get(); + + try { + samlLogoutHandler.onLogoutSuccess(request, response, samlAuth); + } catch (Exception e) { + log.error("SP-initiated SLO failed, falling back to local logout", e); + getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); + } + + return true; + } } - - return true; - } - - // Reconstruct Saml2Authentication from JWT claims for SLO - Optional reconstructedAuth = - reconstructSaml2AuthenticationFromJwt(request); - - if (reconstructedAuth.isPresent()) { - Saml2Authentication samlAuth = reconstructedAuth.get(); - - try { - samlLogoutHandler.onLogoutSuccess(request, response, samlAuth); - } catch (Exception e) { - log.error("SAML SLO failed, falling back to local logout", e); - getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); - } - + } else { + getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); return true; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java index 8710ed2641..32f57e3c2c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java @@ -53,9 +53,6 @@ public class OAuth2Configuration { ApplicationProperties applicationProperties, @Lazy UserService userService) { this.userService = userService; this.applicationProperties = applicationProperties; - log.info( - "OAuth2Configuration initialized - OAuth2 enabled: {}", - applicationProperties.getSecurity().getOauth2().getEnabled()); } @Bean @@ -222,8 +219,6 @@ public class OAuth2Configuration { name, oauth.getIssuer(), REDIRECT_URI_PATH + name); - } else { - log.warn("OIDC OAuth2 provider validation failed - provider will not be registered"); } return isValid diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java index 4e6a1d63aa..a9a362acf1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java @@ -44,6 +44,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties.Security.SAML2; +import stirling.software.common.util.GeneralUtils; import stirling.software.proprietary.security.service.JwtServiceInterface; @Configuration @@ -59,7 +60,7 @@ public class Saml2Configuration { @Bean @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") - public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { + public RelyingPartyRegistrationRepository relyingPartyRegistrations() { SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); Optional metadataInfo = loadIdpMetadata(samlConf); @@ -76,8 +77,8 @@ public class Saml2Configuration { Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); // Load SP private key and certificate - Resource privateKeyResource = samlConf.getPrivateKey(); - Resource certificateResource = samlConf.getSpCert(); + Resource privateKeyResource = samlConf.getSp().getPrivateKeyResource(); + Resource certificateResource = samlConf.getSp().getCertResource(); log.debug("Loading SP private key from: {}", privateKeyResource.getDescription()); if (!privateKeyResource.exists()) { @@ -116,19 +117,19 @@ public class Saml2Configuration { metadataInfo .map(IdpMetadataInfo::entityId) .filter(id -> id != null && !id.isBlank()) - .orElseGet(samlConf::getIdpEntityIdOrIssuer); + .orElseGet(() -> samlConf.getProvider().getEntityId()); String idpSingleLoginUrl = metadataInfo .map(IdpMetadataInfo::singleSignOnServiceUrl) .filter(url -> url != null && !url.isBlank()) - .orElseGet(samlConf::getIdpSingleLoginUrl); + .orElseGet(() -> samlConf.getProvider().getSingleLoginUrl()); String idpSingleLogoutUrl = metadataInfo .map(IdpMetadataInfo::singleLogoutServiceUrl) .filter(url -> url != null && !url.isBlank()) - .orElseGet(samlConf::getIdpSingleLogoutUrl); + .orElseGet(() -> samlConf.getProvider().getSingleLogoutUrl()); // Validate required IdP configuration if (idpEntityId == null || idpEntityId.isBlank()) { @@ -190,7 +191,7 @@ public class Saml2Configuration { log.info( "SAML2 configuration initialized successfully. Registration ID: {}, IdP: {}", samlConf.getRegistrationId(), - samlConf.getIdpIssuer()); + idpEntityId); return new InMemoryRelyingPartyRegistrationRepository(rp); } @@ -249,7 +250,7 @@ public class Saml2Configuration { private X509Certificate loadIdpCertificateFromResource(SAML2 samlConf) { try { - Resource idpCertResource = samlConf.getIdpCert(); + Resource idpCertResource = samlConf.getProvider().getCertResource(); if (idpCertResource == null) { throw new IllegalStateException("SAML2 IdP certificate resource is not defined"); } @@ -274,18 +275,72 @@ public class Saml2Configuration { log.info( "Applying IdP metadata overrides for registration: {}", samlConf.getRegistrationId()); - overrideIfPresent(metadataInfo.entityId(), samlConf::setIdpIssuer); - overrideIfPresent(metadataInfo.singleSignOnServiceUrl(), samlConf::setIdpSingleLoginUrl); - overrideIfPresent(metadataInfo.singleLogoutServiceUrl(), samlConf::setIdpSingleLogoutUrl); + SAML2.Provider provider = samlConf.getProvider(); + overrideIfPresent(metadataInfo.entityId(), provider::setEntityId); + overrideIfPresent(metadataInfo.singleSignOnServiceUrl(), provider::setSingleLoginUrl); + overrideIfPresent(metadataInfo.singleLogoutServiceUrl(), provider::setSingleLogoutUrl); + + // Persist discovered metadata values to settings.yml + persistMetadataToSettings(metadataInfo); + } + + /** + * Persists IdP metadata discovered values to settings.yml. This ensures the discovered + * configuration is saved for future reference and survives restarts even if the metadata + * endpoint becomes unavailable. + */ + private void persistMetadataToSettings(IdpMetadataInfo metadataInfo) { + log.info( + "Migrating discovered IdP metadata to SAML configuration. Existing configuration will be overridden."); + + try { + boolean anyPersisted = false; + + if (hasText(metadataInfo.entityId())) { + GeneralUtils.saveKeyToSettings( + "security.saml2.provider.entityId", metadataInfo.entityId()); + log.info(" -> Persisted provider.entityId: {}", metadataInfo.entityId()); + anyPersisted = true; + } + + if (hasText(metadataInfo.singleSignOnServiceUrl())) { + GeneralUtils.saveKeyToSettings( + "security.saml2.provider.singleLoginUrl", + metadataInfo.singleSignOnServiceUrl()); + log.info( + " -> Persisted provider.singleLoginUrl: {}", + metadataInfo.singleSignOnServiceUrl()); + anyPersisted = true; + } + + if (hasText(metadataInfo.singleLogoutServiceUrl())) { + GeneralUtils.saveKeyToSettings( + "security.saml2.provider.singleLogoutUrl", + metadataInfo.singleLogoutServiceUrl()); + log.info( + " -> Persisted provider.singleLogoutUrl: {}", + metadataInfo.singleLogoutServiceUrl()); + anyPersisted = true; + } + + if (anyPersisted) { + log.info( + "IdP metadata successfully persisted to settings.yml. These values will be used as fallback if metadataUri becomes unavailable."); + } + } catch (Exception e) { + log.warn( + "Failed to persist IdP metadata to settings.yml: {}. SAML will still work but discovered values won't be saved.", + e.getMessage()); + } } private Optional loadIdpMetadata(SAML2 samlConf) { - String metadataLocation = samlConf.getIdpMetadataUriLocation(); + String metadataLocation = samlConf.getEffectiveMetadataUri(); if (metadataLocation == null || metadataLocation.isBlank()) { return Optional.empty(); } - try (InputStream metadataStream = samlConf.getIdpMetadataUri()) { + try (InputStream metadataStream = samlConf.getMetadataUriAsStream()) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); diff --git a/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts b/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts index 37b34e26c0..8b09eff6f5 100644 --- a/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts +++ b/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts @@ -486,12 +486,6 @@ export const SAML2_PROVIDER: Provider = { description: 'Enable SAML2 authentication (Enterprise only)', defaultValue: false, }, - { - key: 'provider', - type: 'text', - label: 'Provider Name', - description: 'The name of your SAML2 provider', - }, { key: 'registrationId', type: 'text', @@ -500,51 +494,66 @@ export const SAML2_PROVIDER: Provider = { defaultValue: 'stirling', }, { - key: 'idpMetadataUri', + key: 'metadataUri', type: 'text', - label: 'IDP Metadata URI', - description: 'The URI for your provider\'s metadata', + label: 'Metadata URI', + description: 'Your IdP\'s metadata URI (recommended - auto-discovers IdP config)', placeholder: 'https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata', }, { - key: 'idpSingleLoginUrl', + key: 'enableSingleLogout', + type: 'switch', + label: 'Enable Single Logout', + description: 'Enable SP-initiated Single Logout (SLO)', + defaultValue: false, + }, + // IdP Provider settings (manual config - only needed if metadataUri not available) + { + key: 'provider.name', type: 'text', - label: 'IDP Single Login URL', - description: 'The URL for initiating SSO', - placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml', + label: 'Provider Name', + description: 'Display name for your IdP (optional, for reference only)', }, { - key: 'idpSingleLogoutUrl', + key: 'provider.singleLoginUrl', type: 'text', - label: 'IDP Single Logout URL', - description: 'The URL for initiating SLO', - placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml', + label: 'SSO URL', + description: 'IdP Single Sign-On URL (auto-discovered from metadata if set)', + placeholder: 'https://dev-XXXXXXXX.okta.com/app/.../sso/saml', }, { - key: 'idpIssuer', + key: 'provider.singleLogoutUrl', type: 'text', - label: 'IDP Issuer', - description: 'The ID of your provider', + label: 'SLO URL', + description: 'IdP Single Logout URL (auto-discovered from metadata if set)', + placeholder: 'https://dev-XXXXXXXX.okta.com/app/.../slo/saml', }, { - key: 'idpCert', + key: 'provider.entityId', type: 'text', - label: 'IDP Certificate', - description: 'The certificate path (e.g., classpath:okta.cert)', + label: 'IdP Entity ID', + description: 'IdP Entity ID (auto-discovered from metadata if set)', + }, + { + key: 'provider.cert', + type: 'text', + label: 'IdP Certificate', + description: 'IdP signing certificate path (auto-discovered from metadata if set)', placeholder: 'classpath:okta.cert', }, + // SP credentials { - key: 'privateKey', + key: 'sp.privateKey', type: 'text', - label: 'Private Key', - description: 'Your private key path', + label: 'SP Private Key', + description: 'Your Service Provider private key path', placeholder: 'classpath:saml-private-key.key', }, { - key: 'spCert', + key: 'sp.cert', type: 'text', label: 'SP Certificate', - description: 'Your signing certificate path', + description: 'Your Service Provider signing certificate path', placeholder: 'classpath:saml-public-cert.crt', }, { diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx index c6c377539e..b6b7d038a0 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -209,9 +209,20 @@ export default function AdminConnectionsSection() { return !!(providerSettings?.clientId); }; + // Helper to get nested value from object using dot notation + const getNestedValue = (obj: Record, path: string): any => { + return path.split('.').reduce((acc, part) => acc?.[part], obj); + }; + const getProviderSettings = (provider: Provider): Record => { if (provider.id === 'saml2') { - return settings?.saml2 || {}; + const saml2 = settings?.saml2 || {}; + // Flatten nested structure to match field keys with dot notation + const result: Record = {}; + provider.fields.forEach((field) => { + result[field.key] = getNestedValue(saml2, field.key); + }); + return result; } if (provider.id === 'smtp') { @@ -297,8 +308,9 @@ export default function AdminConnectionsSection() { const deltaSettings: Record = {}; if (provider.id === 'saml2') { - // SAML2 settings + // SAML2 settings - keys may use dot notation (e.g., 'provider.name') Object.keys(providerSettings).forEach((key) => { + // Key already has dot notation for nested paths, just prepend security.saml2. deltaSettings[`security.saml2.${key}`] = providerSettings[key]; }); } else if (provider.id === 'oauth2-generic') {