mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
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:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -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));
|
||||||
|
|||||||
+10
@@ -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();
|
||||||
|
|||||||
+42
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Regular → Executable
Reference in New Issue
Block a user