more fixes

This commit is contained in:
Anthony Stirling 2024-11-29 08:43:57 +00:00
parent b4837df76c
commit 5171088fca
13 changed files with 324 additions and 124 deletions

View File

@ -21,6 +21,8 @@ ext {
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.36" lombokVersion = "1.18.36"
bouncycastleVersion = "1.79" bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2"
} }
group = "stirling.software" group = "stirling.software"
@ -144,17 +146,18 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.4.1' implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "com.h2database:h2:2.3.232"
constraints { constraints {
implementation "org.opensaml:opensaml-core" implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api" implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-impl" implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
} }
implementation "org.springframework.security:spring-security-saml2-service-provider" implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
implementation 'com.coveo:saml-client:5.0.0' implementation 'com.coveo:saml-client:5.0.0'

View File

@ -3,13 +3,14 @@ package stirling.software.SPDF.EE;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
@Lazy @Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j @Slf4j
public class EEAppConfig { public class EEAppConfig {

View File

@ -25,9 +25,10 @@ public class LicenseKeyChecker {
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) { KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
this.licenseService = licenseService; this.licenseService = licenseService;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.checkLicense();
} }
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds @Scheduled(fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() { public void checkLicensePeriodically() {
checkLicense(); checkLicense();
} }

View File

@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -77,7 +76,4 @@ public class InitialSecuritySetup {
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
} }
} }

View File

@ -1,17 +1,15 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.BufferedReader;
import java.io.IOException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.*; import java.util.*;
import org.opensaml.saml.common.assertion.ValidationContext; import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
@ -31,8 +29,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ClientRegistrations; import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
@ -41,23 +37,17 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@ -81,6 +71,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
@Slf4j @Slf4j
@DependsOn("runningEE")
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
@ -100,7 +91,6 @@ public class SecurityConfiguration {
@Qualifier("runningEE") @Qualifier("runningEE")
public boolean runningEE; public boolean runningEE;
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter; @Autowired private UserAuthenticationFilter userAuthenticationFilter;
@ -112,10 +102,10 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) { if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
} }
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@ -161,6 +151,9 @@ public class SecurityConfiguration {
sessionManagement -> sessionManagement ->
sessionManagement sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionAuthenticationStrategy(
new RegisterSessionAuthenticationStrategy(
sessionRegistry)) // ?
.maximumSessions(10) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
@ -269,28 +262,38 @@ public class SecurityConfiguration {
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ() && runningEE) { if (applicationProperties.getSecurity().isSaml2Activ() && runningEE) {
http.authenticationProvider(samlAuthenticationProvider()) // Configure the authentication provider
.saml2Login(saml2 -> { OpenSaml4AuthenticationProvider authenticationProvider =
try { new OpenSaml4AuthenticationProvider();
saml2 authenticationProvider.setResponseAuthenticationConverter(
.loginPage("/saml2") new CustomSaml2ResponseAuthenticationConverter(userService));
.relyingPartyRegistrationRepository(relyingPartyRegistrations())
//.authenticationRequestResolver(new OpenSaml4AuthenticationRequestResolver( http.authenticationProvider(authenticationProvider)
// relyingPartyRegistrations() .saml2Login(
// )) saml2 -> {
.successHandler( try {
new CustomSaml2AuthenticationSuccessHandler( saml2.loginPage("/saml2")
loginAttemptService, .relyingPartyRegistrationRepository(
applicationProperties, relyingPartyRegistrations())
userService)) .authenticationManager(
.failureHandler( new ProviderManager(authenticationProvider))
new CustomSaml2AuthenticationFailureHandler()) .successHandler(
.permitAll(); new CustomSaml2AuthenticationSuccessHandler(
} catch (Exception e) { loginAttemptService,
e.printStackTrace(); applicationProperties,
} userService))
}); .failureHandler(
new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver(
authenticationRequestResolver(
relyingPartyRegistrations()));
} catch (Exception e) {
log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e);
}
});
} }
} else { } else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) { if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
@ -308,17 +311,29 @@ public class SecurityConfiguration {
return http.build(); return http.build();
} }
// @Bean
// public Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter(
// RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
// OpenSaml4AuthenticationRequestResolver authenticationRequestResolver =
// new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
//
// Saml2WebSsoAuthenticationRequestFilter filter =
// new Saml2WebSsoAuthenticationRequestFilter(
// authenticationRequestResolver
// );
// return filter;
// }
//
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
value = "security.oauth2.enabled", value = "security.saml2.enabled",
havingValue = "true", havingValue = "true",
matchIfMissing = false) matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() { public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter( provider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService)); new CustomSaml2ResponseAuthenticationConverter(userService));
return provider; return provider;
} }
@ -452,6 +467,11 @@ public class SecurityConfiguration {
.build()); .build());
} }
@Bean
public HttpSessionSaml2AuthenticationRequestRepository saml2AuthenticationRequestRepository() {
return new HttpSessionSaml2AuthenticationRequestRepository();
}
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
name = "security.saml2.enabled", name = "security.saml2.enabled",
@ -465,28 +485,103 @@ public class SecurityConfiguration {
Resource privateKeyResource = samlConf.getPrivateKey(); Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert(); Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential = new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) Saml2X509Credential signingCredential =
.assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso/stirlingpdf-saml") new Saml2X509Credential(
.entityId("http://localhost:8080/saml2/service-provider-metadata/stirlingpdf-saml") CertificateUtils.readPrivateKey(privateKeyResource),
.signingX509Credentials(c -> c.add(signingCredential)) CertificateUtils.readCertificate(certificateResource),
.assertingPartyDetails(party -> party Saml2X509CredentialType.SIGNING);
.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(samlConf.getIdpSingleLoginUrl()) RelyingPartyRegistration rp =
.verificationX509Credentials(c -> c.add(verificationCredential)) RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.singleSignOnServiceBinding(Saml2MessageBinding.POST) .assertionConsumerServiceLocation(
.wantAuthnRequestsSigned(true) "{baseUrl}/login/saml2/sso/stirlingpdf-saml")
) .entityId(
.build(); "http://localhost:8080/saml2/service-provider-metadata/stirlingpdf-saml")
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp); return new InMemoryRelyingPartyRegistrationRepository(rp);
} }
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.info("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.info("AuthnRequest ID: {}", authnRequest.getID());
log.info("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.info(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.info("HTTP Request Method: {}", request.getMethod());
log.info("Request URI: {}", request.getRequestURI());
log.info("Request URL: {}", request.getRequestURL().toString());
log.info("Query String: {}", request.getQueryString());
log.info("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.info(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.info("SAML Request Parameters:");
log.info("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.info("RelayState: {}", request.getParameter("RelayState"));
// Log session information if exists
if (request.getSession(false) != null) {
log.info("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.info(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.info(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
public DaoAuthenticationProvider daoAuthenticationProvider() { public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); provider.setUserDetailsService(userDetailsService);

View File

@ -53,13 +53,15 @@ public class UserService implements UserServiceInterface {
@Transactional @Transactional
public void migrateOauth2ToSSO() { public void migrateOauth2ToSSO() {
userRepository.findByAuthenticationTypeIgnoreCase("OAUTH2") userRepository
.forEach(user -> { .findByAuthenticationTypeIgnoreCase("OAUTH2")
user.setAuthenticationType(AuthenticationType.SSO); .forEach(
userRepository.save(user); user -> {
}); user.setAuthenticationType(AuthenticationType.SSO);
userRepository.save(user);
});
} }
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser) public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {

View File

@ -82,8 +82,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
} }
if (userService.usernameExistsIgnoreCase(username) if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username) && userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername( && !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
username, AuthenticationType.SSO)
&& oAuth.getAutoCreateUser()) { && oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;

View File

@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@ -28,15 +30,26 @@ public class CertificateUtils {
} }
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception { public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
try (PemReader pemReader = try (PEMParser pemParser =
new PemReader( new PEMParser(
new InputStreamReader( new InputStreamReader(
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) { privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
byte[] decodedKey = pemObject.getContent(); Object object = pemParser.readObject();
return (RSAPrivateKey) JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey)); if (object instanceof PEMKeyPair) {
// Handle traditional RSA private key format
PEMKeyPair keypair = (PEMKeyPair) object;
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
} else if (object instanceof PrivateKeyInfo) {
// Handle PKCS#8 format
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
} else {
throw new IllegalArgumentException(
"Unsupported key format: "
+ (object != null ? object.getClass().getName() : "null"));
}
} }
} }
} }

View File

@ -0,0 +1,64 @@
package stirling.software.SPDF.config.security.saml2;
import java.util.Enumeration;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@Component
public class CustomSaml2AuthenticationRequestRepository
implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
private static final String AUTHENTICATION_REQUEST_KEY_PREFIX = "SAML2_AUTHENTICATION_REQUEST_";
@Override
public void saveAuthenticationRequest(
AbstractSaml2AuthenticationRequest authenticationRequest,
HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession(true);
String requestId = authenticationRequest.getId();
session.setAttribute(AUTHENTICATION_REQUEST_KEY_PREFIX + requestId, authenticationRequest);
}
@Override
public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(
HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
Enumeration<String> attributeNames = session.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String attributeName = attributeNames.nextElement();
if (attributeName.startsWith(AUTHENTICATION_REQUEST_KEY_PREFIX)) {
return (AbstractSaml2AuthenticationRequest) session.getAttribute(attributeName);
}
}
}
return null;
}
@Override
public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(
HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession(false);
if (session != null) {
Enumeration<String> attributeNames = session.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String attributeName = attributeNames.nextElement();
if (attributeName.startsWith(AUTHENTICATION_REQUEST_KEY_PREFIX)) {
AbstractSaml2AuthenticationRequest auth =
(AbstractSaml2AuthenticationRequest)
session.getAttribute(attributeName);
session.removeAttribute(attributeName);
return auth;
}
}
}
return null;
}
}

View File

@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService; import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class CustomSaml2AuthenticationSuccessHandler public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler { extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties; private ApplicationProperties applicationProperties;
private UserService userService; private UserService userService;
@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler
throws ServletException, IOException { throws ServletException, IOException {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
log.info("Starting SAML2 authentication success handling");
if (principal instanceof CustomSaml2AuthenticatedPrincipal) { if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName(); String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request log.info("Authenticated principal found for user: {}", username);
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
SavedRequest savedRequest = SavedRequest savedRequest =
@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null; : null;
log.info(
"Session exists: {}, Saved request exists: {}",
session != null,
savedRequest != null);
if (savedRequest != null if (savedRequest != null
&& !RequestUriUtils.isStaticResource( && !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) { contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination log.info(
"Valid saved request found, redirecting to original destination: {}",
savedRequest.getRedirectUrl());
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2(); SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
log.info(
"Processing SAML2 authentication with autoCreateUser: {}",
saml2.getAutoCreateUser());
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
log.info("User {} is blocked due to too many login attempts", username);
if (session != null) { if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST"); session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
} }
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "Your account has been locked due to too many failed login attempts.");
} }
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username) boolean userExists = userService.usernameExistsIgnoreCase(username);
&& !userService.isAuthenticationTypeByUsername( boolean hasPassword = userExists && userService.hasPassword(username);
username, AuthenticationType.SSO) boolean isSSOUser =
&& saml2.getAutoCreateUser()) { userExists
&& userService.isAuthenticationTypeByUsername(
username, AuthenticationType.SSO);
log.info(
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
userExists,
hasPassword,
isSSOUser);
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
log.info(
"User {} exists with password but is not SSO user, redirecting to logout",
username);
response.sendRedirect( response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;
} }
try { try {
if (saml2.getBlockRegistration() if (saml2.getBlockRegistration() && !userExists) {
&& !userService.usernameExistsIgnoreCase(username)) { log.info("Registration blocked for new user: {}", username);
response.sendRedirect( response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user"); contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return; return;
} }
log.info("Processing SSO post-login for user: {}", username);
userService.processSSOPostLogin(username, saml2.getAutoCreateUser()); userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
log.info("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.info(
"Invalid username detected for user: {}, redirecting to logout",
username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return; return;
} }
} }
} else { } else {
log.info("Non-SAML2 principal detected, delegating to parent handler");
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} }
} }

View File

@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2;
import java.util.*; import java.util.*;
import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeStatement;
@ -32,12 +30,12 @@ public class CustomSaml2ResponseAuthenticationConverter
private Map<String, List<Object>> extractAttributes(Assertion assertion) { private Map<String, List<Object>> extractAttributes(Assertion assertion) {
Map<String, List<Object>> attributes = new HashMap<>(); Map<String, List<Object>> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) { for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName(); String attributeName = attribute.getName();
List<Object> values = new ArrayList<>(); List<Object> values = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) { for (XMLObject xmlObject : attribute.getAttributeValues()) {
// Get the text content directly // Get the text content directly
String value = xmlObject.getDOM().getTextContent(); String value = xmlObject.getDOM().getTextContent();
@ -45,7 +43,7 @@ public class CustomSaml2ResponseAuthenticationConverter
values.add(value); values.add(value);
} }
} }
if (!values.isEmpty()) { if (!values.isEmpty()) {
// Store with both full URI and last part of the URI // Store with both full URI and last part of the URI
attributes.put(attributeName, values); attributes.put(attributeName, values);
@ -54,7 +52,7 @@ public class CustomSaml2ResponseAuthenticationConverter
} }
} }
} }
return attributes; return attributes;
} }
@ -62,10 +60,10 @@ public class CustomSaml2ResponseAuthenticationConverter
public Saml2Authentication convert(ResponseToken responseToken) { public Saml2Authentication convert(ResponseToken responseToken) {
Assertion assertion = responseToken.getResponse().getAssertions().get(0); Assertion assertion = responseToken.getResponse().getAssertions().get(0);
Map<String, List<Object>> attributes = extractAttributes(assertion); Map<String, List<Object>> attributes = extractAttributes(assertion);
// Debug log with actual values // Debug log with actual values
log.debug("Extracted SAML Attributes: " + attributes); log.debug("Extracted SAML Attributes: " + attributes);
// Try to get username/identifier in order of preference // Try to get username/identifier in order of preference
String userIdentifier = null; String userIdentifier = null;
if (hasAttribute(attributes, "username")) { if (hasAttribute(attributes, "username")) {
@ -88,7 +86,8 @@ public class CustomSaml2ResponseAuthenticationConverter
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
if (user != null) { if (user != null) {
simpleGrantedAuthority = new SimpleGrantedAuthority(userService.findRole(user).getAuthority()); simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
} }
} }
@ -97,11 +96,9 @@ public class CustomSaml2ResponseAuthenticationConverter
sessionIndexes.add(authnStatement.getSessionIndex()); sessionIndexes.add(authnStatement.getSessionIndex());
} }
CustomSaml2AuthenticatedPrincipal principal = new CustomSaml2AuthenticatedPrincipal( CustomSaml2AuthenticatedPrincipal principal =
userIdentifier, new CustomSaml2AuthenticatedPrincipal(
attributes, userIdentifier, attributes, userIdentifier, sessionIndexes);
userIdentifier,
sessionIndexes);
return new Saml2Authentication( return new Saml2Authentication(
principal, principal,

View File

@ -20,7 +20,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
Optional<User> findByApiKey(String apiKey); Optional<User> findByApiKey(String apiKey);
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType); List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
} }

View File

@ -18,7 +18,7 @@ public class PdfMetadataService {
private final String stirlingPDFLabel; private final String stirlingPDFLabel;
private final UserServiceInterface userService; private final UserServiceInterface userService;
private final boolean runningEE; private final boolean runningEE;
@Autowired @Autowired
public PdfMetadataService( public PdfMetadataService(
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@ -64,10 +64,8 @@ public class PdfMetadataService {
String creator = stirlingPDFLabel; String creator = stirlingPDFLabel;
if (applicationProperties if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
.getEnterpriseEdition() && runningEE) {
.getCustomMetadata()
.isAutoUpdateMetadata() && runningEE) {
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator(); creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
pdf.getDocumentInformation().setProducer(stirlingPDFLabel); pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
@ -86,10 +84,8 @@ public class PdfMetadataService {
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance()); pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
String author = pdfMetadata.getAuthor(); String author = pdfMetadata.getAuthor();
if (applicationProperties if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
.getEnterpriseEdition() && runningEE) {
.getCustomMetadata()
.isAutoUpdateMetadata() && runningEE) {
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor(); author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
if (userService != null) { if (userService != null) {