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

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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();

View File

@@ -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;
}
}

0
testing/compose/start-saml-test.sh Normal file → Executable file
View File