From 5df466266a3687990740e5c54dde38daa2845d7c Mon Sep 17 00:00:00 2001 From: James Brunton Date: Wed, 11 Feb 2026 16:07:06 +0000 Subject: [PATCH] Enhance SSO SAML in desktop app (#5705) # Description of Changes Change the SAML support for SSO to understand when a request is coming from the desktop app, and use the alternate auth flow that the desktop app requires. --- .../common/model/ApplicationProperties.java | 20 +++++---- .../SPDF/config/MultipartConfiguration.java | 21 ++++++---- ...stomSaml2AuthenticationFailureHandler.java | 40 ++++++++++++++++++ ...stomSaml2AuthenticationSuccessHandler.java | 15 ++++++- .../security/saml2/Saml2Configuration.java | 10 +++++ .../security/saml2/TauriSamlUtils.java | 42 +++++++++++++++++++ testing/compose/start-saml-test.sh | 0 7 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/TauriSamlUtils.java mode change 100644 => 100755 testing/compose/start-saml-test.sh 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 2ea611f3a..fcf72d6ae 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 @@ -29,14 +29,14 @@ import org.springframework.stereotype.Component; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.PostConstruct; + import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; -import jakarta.annotation.PostConstruct; - import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.configuration.YamlPropertySourceFactory; import stirling.software.common.model.exception.UnsupportedProviderException; @@ -101,8 +101,8 @@ public class ApplicationProperties { } /** - * Initialize fileUploadLimit from environment variables if not set in settings.yml. - * Supports SYSTEMFILEUPLOADLIMIT (format: "100MB") and SYSTEM_MAXFILESIZE (format: "100" in MB). + * Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports + * SYSTEMFILEUPLOADLIMIT (format: "100MB") and SYSTEM_MAXFILESIZE (format: "100" in MB). */ @PostConstruct public void initializeFileUploadLimitFromEnv() { @@ -124,12 +124,18 @@ public class ApplicationProperties { long sizeInMB = Long.parseLong(systemMaxFileSize.trim()); if (sizeInMB > 0 && sizeInMB <= 999) { fileUploadLimit = sizeInMB + "MB"; - log.info("Setting fileUploadLimit from SYSTEM_MAXFILESIZE: {}MB", sizeInMB); + log.info( + "Setting fileUploadLimit from SYSTEM_MAXFILESIZE: {}MB", + sizeInMB); } else { - log.warn("SYSTEM_MAXFILESIZE value {} is out of valid range (1-999), ignoring", sizeInMB); + log.warn( + "SYSTEM_MAXFILESIZE value {} is out of valid range (1-999), ignoring", + sizeInMB); } } catch (NumberFormatException e) { - log.warn("SYSTEM_MAXFILESIZE value '{}' is not a valid number, ignoring", systemMaxFileSize); + log.warn( + "SYSTEM_MAXFILESIZE value '{}' is not a valid number, ignoring", + systemMaxFileSize); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java index e7e4a2b58..e57b3d5af 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java @@ -14,9 +14,9 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.controller.web.UploadLimitService; /** - * Configuration for Spring multipart file upload settings. - * Synchronizes multipart limits with fileUploadLimit from settings.yml or environment variables - * (SYSTEMFILEUPLOADLIMIT or SYSTEM_MAXFILESIZE). + * Configuration for Spring multipart file upload settings. Synchronizes multipart limits with + * fileUploadLimit from settings.yml or environment variables (SYSTEMFILEUPLOADLIMIT or + * SYSTEM_MAXFILESIZE). */ @Configuration @Slf4j @@ -25,9 +25,9 @@ public class MultipartConfiguration { @Autowired private UploadLimitService uploadLimitService; /** - * Creates MultipartConfigElement that respects fileUploadLimit from settings.yml - * or environment variables (SYSTEMFILEUPLOADLIMIT or SYSTEM_MAXFILESIZE). - * Depends on ApplicationProperties being initialized so @PostConstruct has run. + * Creates MultipartConfigElement that respects fileUploadLimit from settings.yml or environment + * variables (SYSTEMFILEUPLOADLIMIT or SYSTEM_MAXFILESIZE). Depends on ApplicationProperties + * being initialized so @PostConstruct has run. */ @Bean @DependsOn("applicationProperties") @@ -35,7 +35,8 @@ public class MultipartConfiguration { MultipartConfigFactory factory = new MultipartConfigFactory(); // First check if SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE is explicitly set - String springMaxFileSize = java.lang.System.getenv("SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE"); + String springMaxFileSize = + java.lang.System.getenv("SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE"); long uploadLimitBytes = 0; if (springMaxFileSize != null && !springMaxFileSize.trim().isEmpty()) { @@ -45,7 +46,10 @@ public class MultipartConfiguration { uploadLimitBytes = dataSize.toBytes(); log.info("Using SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {}", springMaxFileSize); } catch (Exception e) { - log.warn("Failed to parse SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {}", springMaxFileSize, e); + log.warn( + "Failed to parse SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {}", + springMaxFileSize, + e); } } @@ -73,4 +77,3 @@ public class MultipartConfiguration { return factory.createMultipartConfig(); } } - diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationFailureHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationFailureHandler.java index 21c1da953..e6f5bc388 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationFailureHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationFailureHandler.java @@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.Audited; +import stirling.software.proprietary.security.oauth2.TauriOAuthUtils; @Slf4j @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") @@ -33,9 +34,33 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica if (exception instanceof Saml2AuthenticationException) { Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error(); + if (TauriSamlUtils.isTauriRelayState(request)) { + String redirectUrl = + TauriOAuthUtils.defaultTauriCallbackPath(request.getContextPath()); + String nonce = TauriSamlUtils.extractNonceFromRequest(request); + if (nonce != null) { + redirectUrl = appendQueryParam(redirectUrl, "nonce", nonce); + } + redirectUrl = appendQueryParam(redirectUrl, "errorOAuth", error.getErrorCode()); + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + return; + } getRedirectStrategy() .sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode()); } else if (exception instanceof ProviderNotFoundException) { + if (TauriSamlUtils.isTauriRelayState(request)) { + String redirectUrl = + TauriOAuthUtils.defaultTauriCallbackPath(request.getContextPath()); + String nonce = TauriSamlUtils.extractNonceFromRequest(request); + if (nonce != null) { + redirectUrl = appendQueryParam(redirectUrl, "nonce", nonce); + } + redirectUrl = + appendQueryParam( + redirectUrl, "errorOAuth", "not_authentication_provider_found"); + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + return; + } getRedirectStrategy() .sendRedirect( request, @@ -43,4 +68,19 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica "/login?errorOAuth=not_authentication_provider_found"); } } + + private String appendQueryParam(String path, String key, String value) { + if (path == null || path.isBlank()) { + return path; + } + String separator = path.contains("?") ? "&" : "?"; + String encodedKey = + java.net.URLEncoder.encode(key, java.nio.charset.StandardCharsets.UTF_8); + String encodedValue = + value == null + ? "" + : java.net.URLEncoder.encode( + value, java.nio.charset.StandardCharsets.UTF_8); + return path + separator + encodedKey + "=" + encodedValue; + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index 5f2ce254f..8076829ec 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -33,6 +33,7 @@ import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.Audited; import stirling.software.proprietary.security.model.AuthenticationType; +import stirling.software.proprietary.security.oauth2.TauriOAuthUtils; import stirling.software.proprietary.security.service.JwtServiceInterface; import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.UserService; @@ -233,7 +234,16 @@ public class CustomSaml2AuthenticationSuccessHandler String redirectPath = resolveRedirectPath(request, contextPath); String origin = resolveOrigin(request); clearRedirectCookie(response); - return origin + redirectPath + "#access_token=" + jwt; + String url = origin + redirectPath + "#access_token=" + jwt; + + String nonce = TauriSamlUtils.extractNonceFromRequest(request); + if (nonce != null) { + url += + "&nonce=" + + java.net.URLEncoder.encode( + nonce, java.nio.charset.StandardCharsets.UTF_8); + } + return url; } /** @@ -256,6 +266,9 @@ public class CustomSaml2AuthenticationSuccessHandler } private String resolveRedirectPath(HttpServletRequest request, String contextPath) { + if (TauriSamlUtils.isTauriRelayState(request)) { + return TauriOAuthUtils.defaultTauriCallbackPath(contextPath); + } return extractRedirectPathFromCookie(request) .filter(path -> path.startsWith("/")) .orElseGet(() -> defaultCallbackPath(contextPath)); 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 9cbde954f..411ffe107 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 @@ -156,6 +156,16 @@ public class Saml2Configuration { OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); + resolver.setRelayStateResolver( + request -> { + String tauriParam = request.getParameter("tauri"); + if (!"1".equals(tauriParam)) { + return null; + } + String nonce = request.getParameter("nonce"); + return TauriSamlUtils.buildRelayState(nonce); + }); + resolver.setAuthnRequestCustomizer( customizer -> { HttpServletRequest request = customizer.getRequest(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/TauriSamlUtils.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/TauriSamlUtils.java new file mode 100644 index 000000000..c20707b5a --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/TauriSamlUtils.java @@ -0,0 +1,42 @@ +package stirling.software.proprietary.security.saml2; + +import jakarta.servlet.http.HttpServletRequest; + +/** Utility helpers for the Tauri desktop SAML flow. */ +public final class TauriSamlUtils { + + public static final String TAURI_RELAY_STATE_PREFIX = "tauri:"; + + private TauriSamlUtils() { + // Utility class - prevent instantiation + } + + public static boolean isTauriRelayState(HttpServletRequest request) { + String relayState = request.getParameter("RelayState"); + return relayState != null + && (relayState.equals("tauri") || relayState.startsWith(TAURI_RELAY_STATE_PREFIX)); + } + + public static String extractNonceFromRelayState(String relayState) { + if (relayState == null || !relayState.startsWith(TAURI_RELAY_STATE_PREFIX)) { + return null; + } + String[] parts = relayState.split(":"); + if (parts.length >= 2) { + String nonce = parts[parts.length - 1]; + return nonce.isBlank() ? null : nonce; + } + return null; + } + + public static String extractNonceFromRequest(HttpServletRequest request) { + return extractNonceFromRelayState(request.getParameter("RelayState")); + } + + public static String buildRelayState(String nonce) { + if (nonce == null || nonce.isBlank()) { + return "tauri"; + } + return TAURI_RELAY_STATE_PREFIX + nonce; + } +} diff --git a/testing/compose/start-saml-test.sh b/testing/compose/start-saml-test.sh old mode 100644 new mode 100755