diff --git a/build.gradle b/build.gradle index fdfb5c8f..a33a3d3a 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,8 @@ ext { imageioVersion = "3.12.0" lombokVersion = "1.18.36" bouncycastleVersion = "1.79" + springSecuritySamlVersion = "6.4.1" + openSamlVersion = "4.3.2" } 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-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' // Don't upgrade h2database runtimeOnly "com.h2database:h2:2.3.232" constraints { - implementation "org.opensaml:opensaml-core" - implementation "org.opensaml:opensaml-saml-api" - implementation "org.opensaml:opensaml-saml-impl" + implementation "org.opensaml:opensaml-core:$openSamlVersion" + implementation "org.opensaml:opensaml-saml-api:$openSamlVersion" + 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' diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index 8c3c4fd0..b5fb3556 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -3,13 +3,14 @@ package stirling.software.SPDF.EE; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; 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 stirling.software.SPDF.model.ApplicationProperties; @Configuration -@Lazy +@Order(Ordered.HIGHEST_PRECEDENCE) @Slf4j public class EEAppConfig { diff --git a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java index 3da7da05..9217e7ce 100644 --- a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java +++ b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java @@ -25,9 +25,10 @@ public class LicenseKeyChecker { KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) { this.licenseService = licenseService; this.applicationProperties = applicationProperties; + this.checkLicense(); } - @Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds + @Scheduled(fixedRate = 604800000) // 7 days in milliseconds public void checkLicensePeriodically() { checkLicense(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index b692d4f1..7e542a00 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; -import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.model.ApplicationProperties; @@ -77,7 +76,4 @@ public class InitialSecuritySetup { log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); } } - - - } diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index be3d8e30..30710c16 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -1,17 +1,15 @@ package stirling.software.SPDF.config.security; -import java.io.BufferedReader; -import java.io.IOException; import java.security.cert.X509Certificate; -import java.time.Duration; 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.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; import org.springframework.core.io.Resource; 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.InMemoryClientRegistrationRepository; 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.Saml2X509CredentialType; 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.Saml2MessageBinding; 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.Saml2WebSsoAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 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.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.savedrequest.NullRequestCache; 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.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; @@ -81,6 +71,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl; @EnableWebSecurity @EnableMethodSecurity @Slf4j +@DependsOn("runningEE") public class SecurityConfiguration { @Autowired private CustomUserDetailsService userDetailsService; @@ -100,7 +91,6 @@ public class SecurityConfiguration { @Qualifier("runningEE") public boolean runningEE; - @Autowired ApplicationProperties applicationProperties; @Autowired private UserAuthenticationFilter userAuthenticationFilter; @@ -112,10 +102,10 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - if (applicationProperties.getSecurity().getCsrfDisabled()) { + if (applicationProperties.getSecurity().getCsrfDisabled()) { http.csrf(csrf -> csrf.disable()); - } - + } + if (loginEnabledValue) { http.addFilterBefore( userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -161,6 +151,9 @@ public class SecurityConfiguration { sessionManagement -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .sessionAuthenticationStrategy( + new RegisterSessionAuthenticationStrategy( + sessionRegistry)) // ? .maximumSessions(10) .maxSessionsPreventsLogin(false) .sessionRegistry(sessionRegistry) @@ -269,28 +262,38 @@ public class SecurityConfiguration { // Handle SAML if (applicationProperties.getSecurity().isSaml2Activ() && runningEE) { - http.authenticationProvider(samlAuthenticationProvider()) - .saml2Login(saml2 -> { - try { - saml2 - .loginPage("/saml2") - .relyingPartyRegistrationRepository(relyingPartyRegistrations()) - //.authenticationRequestResolver(new OpenSaml4AuthenticationRequestResolver( - // relyingPartyRegistrations() - // )) - .successHandler( - new CustomSaml2AuthenticationSuccessHandler( - loginAttemptService, - applicationProperties, - userService)) - .failureHandler( - new CustomSaml2AuthenticationFailureHandler()) - .permitAll(); - } catch (Exception e) { - e.printStackTrace(); - } - }); + // Configure the authentication provider + OpenSaml4AuthenticationProvider authenticationProvider = + new OpenSaml4AuthenticationProvider(); + authenticationProvider.setResponseAuthenticationConverter( + new CustomSaml2ResponseAuthenticationConverter(userService)); + + http.authenticationProvider(authenticationProvider) + .saml2Login( + saml2 -> { + try { + saml2.loginPage("/saml2") + .relyingPartyRegistrationRepository( + relyingPartyRegistrations()) + .authenticationManager( + new ProviderManager(authenticationProvider)) + .successHandler( + new CustomSaml2AuthenticationSuccessHandler( + loginAttemptService, + applicationProperties, + userService)) + .failureHandler( + new CustomSaml2AuthenticationFailureHandler()) + .authenticationRequestResolver( + authenticationRequestResolver( + relyingPartyRegistrations())); + } catch (Exception e) { + log.error("Error configuring SAML2 login", e); + throw new RuntimeException(e); + } + }); } + } else { if (!applicationProperties.getSecurity().getCsrfDisabled()) { CookieCsrfTokenRepository cookieRepo = @@ -308,17 +311,29 @@ public class SecurityConfiguration { return http.build(); } - - + + // @Bean + // public Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter( + // RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { + // OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = + // new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); + // + // Saml2WebSsoAuthenticationRequestFilter filter = + // new Saml2WebSsoAuthenticationRequestFilter( + // authenticationRequestResolver + // ); + // return filter; + // } + // @Bean @ConditionalOnProperty( - value = "security.oauth2.enabled", + value = "security.saml2.enabled", havingValue = "true", matchIfMissing = false) public AuthenticationProvider samlAuthenticationProvider() { OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); provider.setResponseAuthenticationConverter( - new CustomSaml2ResponseAuthenticationConverter(userService)); + new CustomSaml2ResponseAuthenticationConverter(userService)); return provider; } @@ -452,6 +467,11 @@ public class SecurityConfiguration { .build()); } + @Bean + public HttpSessionSaml2AuthenticationRequestRepository saml2AuthenticationRequestRepository() { + return new HttpSessionSaml2AuthenticationRequestRepository(); + } + @Bean @ConditionalOnProperty( name = "security.saml2.enabled", @@ -465,28 +485,103 @@ public class SecurityConfiguration { Resource privateKeyResource = samlConf.getPrivateKey(); Resource certificateResource = samlConf.getSpCert(); - - Saml2X509Credential signingCredential = new Saml2X509Credential( - CertificateUtils.readPrivateKey(privateKeyResource), - CertificateUtils.readCertificate(certificateResource), - Saml2X509CredentialType.SIGNING); - RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) - .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso/stirlingpdf-saml") - .entityId("http://localhost:8080/saml2/service-provider-metadata/stirlingpdf-saml") - .signingX509Credentials(c -> c.add(signingCredential)) - .assertingPartyDetails(party -> party - .entityId(samlConf.getIdpIssuer()) - .singleSignOnServiceLocation(samlConf.getIdpSingleLoginUrl()) - .verificationX509Credentials(c -> c.add(verificationCredential)) - .singleSignOnServiceBinding(Saml2MessageBinding.POST) - .wantAuthnRequestsSigned(true) - ) - .build(); + Saml2X509Credential signingCredential = + new Saml2X509Credential( + CertificateUtils.readPrivateKey(privateKeyResource), + CertificateUtils.readCertificate(certificateResource), + Saml2X509CredentialType.SIGNING); + + RelyingPartyRegistration rp = + RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) + .assertionConsumerServiceLocation( + "{baseUrl}/login/saml2/sso/stirlingpdf-saml") + .entityId( + "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); } + @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() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index e9bd229f..d7f35d38 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -53,13 +53,15 @@ public class UserService implements UserServiceInterface { @Transactional public void migrateOauth2ToSSO() { - userRepository.findByAuthenticationTypeIgnoreCase("OAUTH2") - .forEach(user -> { - user.setAuthenticationType(AuthenticationType.SSO); - userRepository.save(user); - }); + userRepository + .findByAuthenticationTypeIgnoreCase("OAUTH2") + .forEach( + user -> { + user.setAuthenticationType(AuthenticationType.SSO); + userRepository.save(user); + }); } - + // Handle OAUTH2 login and user auto creation. public boolean processSSOPostLogin(String username, boolean autoCreateUser) throws IllegalArgumentException, IOException { diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java index 64618b2f..9f3f6e35 100644 --- a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java @@ -82,8 +82,7 @@ public class CustomOAuth2AuthenticationSuccessHandler } if (userService.usernameExistsIgnoreCase(username) && userService.hasPassword(username) - && !userService.isAuthenticationTypeByUsername( - username, AuthenticationType.SSO) + && !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO) && oAuth.getAutoCreateUser()) { response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); return; diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java index 4f0d2488..8ec51226 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java @@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2; import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.security.KeyFactory; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; 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.PemReader; import org.springframework.core.io.Resource; @@ -28,15 +30,26 @@ public class CertificateUtils { } public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception { - try (PemReader pemReader = - new PemReader( + try (PEMParser pemParser = + new PEMParser( new InputStreamReader( privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) { - PemObject pemObject = pemReader.readPemObject(); - byte[] decodedKey = pemObject.getContent(); - return (RSAPrivateKey) - KeyFactory.getInstance("RSA") - .generatePrivate(new PKCS8EncodedKeySpec(decodedKey)); + + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + + 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")); + } } } } diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationRequestRepository.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationRequestRepository.java new file mode 100644 index 00000000..b7517480 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationRequestRepository.java @@ -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 { + + 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 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 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; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index b3da75a6..e0030581 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.security.LoginAttemptService; import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.ApplicationProperties; @@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.utils.RequestUriUtils; @AllArgsConstructor +@Slf4j public class CustomSaml2AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private LoginAttemptService loginAttemptService; - private ApplicationProperties applicationProperties; private UserService userService; @@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler throws ServletException, IOException { Object principal = authentication.getPrincipal(); + log.info("Starting SAML2 authentication success handling"); if (principal instanceof CustomSaml2AuthenticatedPrincipal) { String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName(); - // Get the saved request + log.info("Authenticated principal found for user: {}", username); + HttpSession session = request.getSession(false); String contextPath = request.getContextPath(); SavedRequest savedRequest = @@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null; + log.info( + "Session exists: {}, Saved request exists: {}", + session != null, + savedRequest != null); + if (savedRequest != null && !RequestUriUtils.isStaticResource( 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); } else { SAML2 saml2 = applicationProperties.getSecurity().getSaml2(); + log.info( + "Processing SAML2 authentication with autoCreateUser: {}", + saml2.getAutoCreateUser()); if (loginAttemptService.isBlocked(username)) { + log.info("User {} is blocked due to too many login attempts", username); if (session != null) { session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST"); } throw new LockedException( "Your account has been locked due to too many failed login attempts."); } - if (userService.usernameExistsIgnoreCase(username) - && userService.hasPassword(username) - && !userService.isAuthenticationTypeByUsername( - username, AuthenticationType.SSO) - && saml2.getAutoCreateUser()) { + + boolean userExists = userService.usernameExistsIgnoreCase(username); + boolean hasPassword = userExists && userService.hasPassword(username); + boolean isSSOUser = + 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( contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); return; } + try { - if (saml2.getBlockRegistration() - && !userService.usernameExistsIgnoreCase(username)) { + if (saml2.getBlockRegistration() && !userExists) { + log.info("Registration blocked for new user: {}", username); response.sendRedirect( contextPath + "/login?erroroauth=oauth2_admin_blocked_user"); return; } + log.info("Processing SSO post-login for user: {}", username); userService.processSSOPostLogin(username, saml2.getAutoCreateUser()); + log.info("Successfully processed authentication for user: {}", username); response.sendRedirect(contextPath + "/"); return; } catch (IllegalArgumentException e) { + log.info( + "Invalid username detected for user: {}, redirecting to logout", + username); response.sendRedirect(contextPath + "/logout?invalidUsername=true"); return; } } } else { + log.info("Non-SAML2 principal detected, delegating to parent handler"); super.onAuthenticationSuccess(request, response, authentication); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2ResponseAuthenticationConverter.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2ResponseAuthenticationConverter.java index 2f0caa47..bfd35c64 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2ResponseAuthenticationConverter.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2ResponseAuthenticationConverter.java @@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2; import java.util.*; 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.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; @@ -32,12 +30,12 @@ public class CustomSaml2ResponseAuthenticationConverter private Map> extractAttributes(Assertion assertion) { Map> attributes = new HashMap<>(); - + for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { for (Attribute attribute : attributeStatement.getAttributes()) { String attributeName = attribute.getName(); List values = new ArrayList<>(); - + for (XMLObject xmlObject : attribute.getAttributeValues()) { // Get the text content directly String value = xmlObject.getDOM().getTextContent(); @@ -45,7 +43,7 @@ public class CustomSaml2ResponseAuthenticationConverter values.add(value); } } - + if (!values.isEmpty()) { // Store with both full URI and last part of the URI attributes.put(attributeName, values); @@ -54,7 +52,7 @@ public class CustomSaml2ResponseAuthenticationConverter } } } - + return attributes; } @@ -62,10 +60,10 @@ public class CustomSaml2ResponseAuthenticationConverter public Saml2Authentication convert(ResponseToken responseToken) { Assertion assertion = responseToken.getResponse().getAssertions().get(0); Map> attributes = extractAttributes(assertion); - + // Debug log with actual values log.debug("Extracted SAML Attributes: " + attributes); - + // Try to get username/identifier in order of preference String userIdentifier = null; if (hasAttribute(attributes, "username")) { @@ -88,7 +86,8 @@ public class CustomSaml2ResponseAuthenticationConverter if (userOpt.isPresent()) { User user = userOpt.get(); 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()); } - CustomSaml2AuthenticatedPrincipal principal = new CustomSaml2AuthenticatedPrincipal( - userIdentifier, - attributes, - userIdentifier, - sessionIndexes); + CustomSaml2AuthenticatedPrincipal principal = + new CustomSaml2AuthenticatedPrincipal( + userIdentifier, attributes, userIdentifier, sessionIndexes); return new Saml2Authentication( principal, diff --git a/src/main/java/stirling/software/SPDF/repository/UserRepository.java b/src/main/java/stirling/software/SPDF/repository/UserRepository.java index 6e63911c..e1f53efb 100644 --- a/src/main/java/stirling/software/SPDF/repository/UserRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/UserRepository.java @@ -20,7 +20,6 @@ public interface UserRepository extends JpaRepository { Optional findByUsername(String username); Optional findByApiKey(String apiKey); - + List findByAuthenticationTypeIgnoreCase(String authenticationType); - } diff --git a/src/main/java/stirling/software/SPDF/service/PdfMetadataService.java b/src/main/java/stirling/software/SPDF/service/PdfMetadataService.java index 61375a5a..f6e8a5b5 100644 --- a/src/main/java/stirling/software/SPDF/service/PdfMetadataService.java +++ b/src/main/java/stirling/software/SPDF/service/PdfMetadataService.java @@ -18,7 +18,7 @@ public class PdfMetadataService { private final String stirlingPDFLabel; private final UserServiceInterface userService; private final boolean runningEE; - + @Autowired public PdfMetadataService( ApplicationProperties applicationProperties, @@ -64,10 +64,8 @@ public class PdfMetadataService { String creator = stirlingPDFLabel; - if (applicationProperties - .getEnterpriseEdition() - .getCustomMetadata() - .isAutoUpdateMetadata() && runningEE) { + if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata() + && runningEE) { creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator(); pdf.getDocumentInformation().setProducer(stirlingPDFLabel); @@ -86,10 +84,8 @@ public class PdfMetadataService { pdf.getDocumentInformation().setModificationDate(Calendar.getInstance()); String author = pdfMetadata.getAuthor(); - if (applicationProperties - .getEnterpriseEdition() - .getCustomMetadata() - .isAutoUpdateMetadata() && runningEE) { + if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata() + && runningEE) { author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor(); if (userService != null) {