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.
This commit is contained in:
James Brunton
2026-02-11 16:07:06 +00:00
committed by GitHub
parent cc1931fa75
commit 5df466266a
7 changed files with 131 additions and 17 deletions
@@ -29,14 +29,14 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.PostConstruct;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.PostConstruct;
import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.configuration.YamlPropertySourceFactory; import stirling.software.common.configuration.YamlPropertySourceFactory;
import stirling.software.common.model.exception.UnsupportedProviderException; 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. * Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports
* Supports SYSTEMFILEUPLOADLIMIT (format: "100MB") and SYSTEM_MAXFILESIZE (format: "100" in MB). * SYSTEMFILEUPLOADLIMIT (format: "100MB") and SYSTEM_MAXFILESIZE (format: "100" in MB).
*/ */
@PostConstruct @PostConstruct
public void initializeFileUploadLimitFromEnv() { public void initializeFileUploadLimitFromEnv() {
@@ -124,12 +124,18 @@ public class ApplicationProperties {
long sizeInMB = Long.parseLong(systemMaxFileSize.trim()); long sizeInMB = Long.parseLong(systemMaxFileSize.trim());
if (sizeInMB > 0 && sizeInMB <= 999) { if (sizeInMB > 0 && sizeInMB <= 999) {
fileUploadLimit = sizeInMB + "MB"; fileUploadLimit = sizeInMB + "MB";
log.info("Setting fileUploadLimit from SYSTEM_MAXFILESIZE: {}MB", sizeInMB); log.info(
"Setting fileUploadLimit from SYSTEM_MAXFILESIZE: {}MB",
sizeInMB);
} else { } 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) { } 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);
} }
} }
} }
@@ -14,9 +14,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.controller.web.UploadLimitService; import stirling.software.SPDF.controller.web.UploadLimitService;
/** /**
* Configuration for Spring multipart file upload settings. * Configuration for Spring multipart file upload settings. Synchronizes multipart limits with
* Synchronizes multipart limits with fileUploadLimit from settings.yml or environment variables * fileUploadLimit from settings.yml or environment variables (SYSTEMFILEUPLOADLIMIT or
* (SYSTEMFILEUPLOADLIMIT or SYSTEM_MAXFILESIZE). * SYSTEM_MAXFILESIZE).
*/ */
@Configuration @Configuration
@Slf4j @Slf4j
@@ -25,9 +25,9 @@ public class MultipartConfiguration {
@Autowired private UploadLimitService uploadLimitService; @Autowired private UploadLimitService uploadLimitService;
/** /**
* Creates MultipartConfigElement that respects fileUploadLimit from settings.yml * Creates MultipartConfigElement that respects fileUploadLimit from settings.yml or environment
* or environment variables (SYSTEMFILEUPLOADLIMIT or SYSTEM_MAXFILESIZE). * variables (SYSTEMFILEUPLOADLIMIT or SYSTEM_MAXFILESIZE). Depends on ApplicationProperties
* Depends on ApplicationProperties being initialized so @PostConstruct has run. * being initialized so @PostConstruct has run.
*/ */
@Bean @Bean
@DependsOn("applicationProperties") @DependsOn("applicationProperties")
@@ -35,7 +35,8 @@ public class MultipartConfiguration {
MultipartConfigFactory factory = new MultipartConfigFactory(); MultipartConfigFactory factory = new MultipartConfigFactory();
// First check if SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE is explicitly set // 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; long uploadLimitBytes = 0;
if (springMaxFileSize != null && !springMaxFileSize.trim().isEmpty()) { if (springMaxFileSize != null && !springMaxFileSize.trim().isEmpty()) {
@@ -45,7 +46,10 @@ public class MultipartConfiguration {
uploadLimitBytes = dataSize.toBytes(); uploadLimitBytes = dataSize.toBytes();
log.info("Using SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {}", springMaxFileSize); log.info("Using SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: {}", springMaxFileSize);
} catch (Exception e) { } 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(); return factory.createMultipartConfig();
} }
} }
@@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.audit.Audited; import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.security.oauth2.TauriOAuthUtils;
@Slf4j @Slf4j
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
@@ -33,9 +34,33 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
if (exception instanceof Saml2AuthenticationException) { if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error(); 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() getRedirectStrategy()
.sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode()); .sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) { } 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() getRedirectStrategy()
.sendRedirect( .sendRedirect(
request, request,
@@ -43,4 +68,19 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
"/login?errorOAuth=not_authentication_provider_found"); "/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;
}
} }
@@ -33,6 +33,7 @@ import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.audit.Audited; import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.security.model.AuthenticationType; 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.JwtServiceInterface;
import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@@ -233,7 +234,16 @@ public class CustomSaml2AuthenticationSuccessHandler
String redirectPath = resolveRedirectPath(request, contextPath); String redirectPath = resolveRedirectPath(request, contextPath);
String origin = resolveOrigin(request); String origin = resolveOrigin(request);
clearRedirectCookie(response); 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) { private String resolveRedirectPath(HttpServletRequest request, String contextPath) {
if (TauriSamlUtils.isTauriRelayState(request)) {
return TauriOAuthUtils.defaultTauriCallbackPath(contextPath);
}
return extractRedirectPathFromCookie(request) return extractRedirectPathFromCookie(request)
.filter(path -> path.startsWith("/")) .filter(path -> path.startsWith("/"))
.orElseGet(() -> defaultCallbackPath(contextPath)); .orElseGet(() -> defaultCallbackPath(contextPath));
@@ -156,6 +156,16 @@ public class Saml2Configuration {
OpenSaml4AuthenticationRequestResolver resolver = OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); 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( resolver.setAuthnRequestCustomizer(
customizer -> { customizer -> {
HttpServletRequest request = customizer.getRequest(); HttpServletRequest request = customizer.getRequest();
@@ -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;
}
}
View File