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 f43baf0a3..b692d4f16 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -7,6 +7,7 @@ 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; @@ -30,6 +31,7 @@ public class InitialSecuritySetup { initializeAdminUser(); } else { databaseBackupHelper.exportDatabase(); + userService.migrateOauth2ToSSO(); } initializeInternalApiUser(); } @@ -75,4 +77,7 @@ 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 2919cf8ae..66e961bae 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -107,13 +107,14 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - + if (applicationProperties.getSecurity().getCsrfDisabled()) { + http.csrf(csrf -> csrf.disable()); + } + if (loginEnabledValue) { http.addFilterBefore( userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - if (applicationProperties.getSecurity().getCsrfDisabled()) { - http.csrf(csrf -> csrf.disable()); - } else { + if (!applicationProperties.getSecurity().getCsrfDisabled()) { CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository.withHttpOnlyFalse(); CsrfTokenRequestAttributeHandler requestHandler = @@ -268,11 +269,10 @@ public class SecurityConfiguration { try { saml2 .loginPage("/saml2") - // Add this .relyingPartyRegistrationRepository(relyingPartyRegistrations()) - .authenticationRequestResolver(new OpenSaml4AuthenticationRequestResolver( - relyingPartyRegistrations() - )) + //.authenticationRequestResolver(new OpenSaml4AuthenticationRequestResolver( + // relyingPartyRegistrations() + // )) .successHandler( new CustomSaml2AuthenticationSuccessHandler( loginAttemptService, @@ -284,16 +284,10 @@ public class SecurityConfiguration { } catch (Exception e) { e.printStackTrace(); } - }) - .saml2Logout(logout -> logout - .logoutUrl("/logout")) - ; - + }); } } else { - if (applicationProperties.getSecurity().getCsrfDisabled()) { - http.csrf(csrf -> csrf.disable()); - } else { + if (!applicationProperties.getSecurity().getCsrfDisabled()) { CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository.withHttpOnlyFalse(); CsrfTokenRequestAttributeHandler requestHandler = @@ -316,19 +310,6 @@ public class SecurityConfiguration { OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); provider.setResponseAuthenticationConverter( new CustomSaml2ResponseAuthenticationConverter(userService)); - - provider.setAssertionValidator(token -> { - try { - HashMap params = new HashMap<>(); - // Add 5 minutes clock skew - params.put(Saml2ErrorCodes.INVALID_ASSERTION, Duration.ofMinutes(5)); - ValidationContext context = new ValidationContext(params); - return Saml2ResponseValidatorResult.success(); - } catch (Exception e) { - return Saml2ResponseValidatorResult.failure(); - } - }); - return provider; } // Client Registration Repository for OAUTH2 OIDC Login @@ -489,7 +470,7 @@ public class SecurityConfiguration { .entityId(samlConf.getIdpIssuer()) .singleSignOnServiceLocation(samlConf.getIdpSingleLoginUrl()) .verificationX509Credentials(c -> c.add(verificationCredential)) - .singleSignOnServiceBinding(Saml2MessageBinding.POST) // Add this + .singleSignOnServiceBinding(Saml2MessageBinding.POST) .wantAuthnRequestsSigned(true) ) .build(); 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 6b1457dc3..e9bd229fa 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -18,6 +18,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; @@ -50,8 +51,17 @@ public class UserService implements UserServiceInterface { @Autowired ApplicationProperties applicationProperties; + @Transactional + public void migrateOauth2ToSSO() { + userRepository.findByAuthenticationTypeIgnoreCase("OAUTH2") + .forEach(user -> { + user.setAuthenticationType(AuthenticationType.SSO); + userRepository.save(user); + }); + } + // Handle OAUTH2 login and user auto creation. - public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) + public boolean processSSOPostLogin(String username, boolean autoCreateUser) throws IllegalArgumentException, IOException { if (!isUsernameValid(username)) { return false; @@ -61,7 +71,7 @@ public class UserService implements UserServiceInterface { return true; } if (autoCreateUser) { - saveUser(username, AuthenticationType.OAUTH2); + saveUser(username, AuthenticationType.SSO); return true; } return false; 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 36c4cfb36..64618b2f9 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 @@ -83,7 +83,7 @@ public class CustomOAuth2AuthenticationSuccessHandler if (userService.usernameExistsIgnoreCase(username) && userService.hasPassword(username) && !userService.isAuthenticationTypeByUsername( - username, AuthenticationType.OAUTH2) + username, AuthenticationType.SSO) && oAuth.getAutoCreateUser()) { response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); return; @@ -95,7 +95,7 @@ public class CustomOAuth2AuthenticationSuccessHandler return; } if (principal instanceof OAuth2User) { - userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser()); + userService.processSSOPostLogin(username, oAuth.getAutoCreateUser()); } response.sendRedirect(contextPath + "/"); return; 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 d4b917581..b3da75a6b 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 @@ -63,7 +63,7 @@ public class CustomSaml2AuthenticationSuccessHandler if (userService.usernameExistsIgnoreCase(username) && userService.hasPassword(username) && !userService.isAuthenticationTypeByUsername( - username, AuthenticationType.OAUTH2) + username, AuthenticationType.SSO) && saml2.getAutoCreateUser()) { response.sendRedirect( contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); @@ -76,7 +76,7 @@ public class CustomSaml2AuthenticationSuccessHandler contextPath + "/login?erroroauth=oauth2_admin_blocked_user"); return; } - userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser()); + userService.processSSOPostLogin(username, saml2.getAutoCreateUser()); response.sendRedirect(contextPath + "/"); return; } catch (IllegalArgumentException e) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index d27534875..c7d19f518 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -244,8 +244,8 @@ public class UserController { return new RedirectView("/addUsers?messageType=invalidRole", true); } - if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) { - userService.saveUser(username, AuthenticationType.OAUTH2, role); + if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { + userService.saveUser(username, AuthenticationType.SSO, role); } else { if (password.isBlank()) { return new RedirectView("/addUsers?messageType=invalidPassword", true); diff --git a/src/main/java/stirling/software/SPDF/model/AuthenticationType.java b/src/main/java/stirling/software/SPDF/model/AuthenticationType.java index 58e7befb0..80419cdd2 100644 --- a/src/main/java/stirling/software/SPDF/model/AuthenticationType.java +++ b/src/main/java/stirling/software/SPDF/model/AuthenticationType.java @@ -2,5 +2,5 @@ package stirling.software.SPDF.model; public enum AuthenticationType { WEB, - OAUTH2 + SSO } diff --git a/src/main/java/stirling/software/SPDF/repository/UserRepository.java b/src/main/java/stirling/software/SPDF/repository/UserRepository.java index 0f5387f79..6e63911c7 100644 --- a/src/main/java/stirling/software/SPDF/repository/UserRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/UserRepository.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -19,4 +20,7 @@ public interface UserRepository extends JpaRepository { Optional findByUsername(String username); Optional findByApiKey(String apiKey); + + List findByAuthenticationTypeIgnoreCase(String authenticationType); + } diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 837acd895..eded10e7e 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -16,7 +16,7 @@ security: csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production) loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1 loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts - loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2) + loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2) initialLogin: username: '' # initial username for the first login password: '' # initial password for the first login @@ -42,14 +42,14 @@ security: issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint clientId: '' # client ID from your provider clientSecret: '' # client secret from your provider - autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users + autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin useAsUsername: email # default is 'email'; custom fields can be used as the username scopes: openid, profile, email # specify the scopes for which the application will request permissions provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak' saml2: - enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true - autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users + enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true) + autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin registrationId: stirling idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js index 1e537dfc7..7c810de0f 100644 --- a/src/main/resources/static/js/fileInput.js +++ b/src/main/resources/static/js/fileInput.js @@ -83,21 +83,22 @@ function setupFileInput(chooser) { $("#" + elementId).on("change", function (e) { let element = e.target; const isDragAndDrop = e.detail?.source == 'drag-drop'; + if (element instanceof HTMLInputElement && element.hasAttribute("multiple")) { - allFiles = isDragAndDrop ? allFiles : [... allFiles, ... element.files]; - } else { - allFiles = Array.from(isDragAndDrop ? allFiles : element.files[0]); - } + allFiles = isDragAndDrop ? allFiles : [...allFiles, ...element.files]; + } else { + allFiles = isDragAndDrop ? allFiles : [element.files[0]]; + } if (!isDragAndDrop) { - let dataTransfer = new DataTransfer(); - allFiles.forEach(file => dataTransfer.items.add(file)); - element.files = dataTransfer.files; + let dataTransfer = new DataTransfer(); + allFiles.forEach(file => dataTransfer.items.add(file)); + element.files = dataTransfer.files; } handleFileInputChange(this); this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true })); - }); +}); function handleFileInputChange(inputElement) { const files = allFiles; diff --git a/src/main/resources/templates/addUsers.html b/src/main/resources/templates/addUsers.html index ba020c4f6..c94fa0416 100644 --- a/src/main/resources/templates/addUsers.html +++ b/src/main/resources/templates/addUsers.html @@ -189,7 +189,7 @@
@@ -267,7 +267,7 @@ var passwordFieldContainer = $('#passwordContainer'); var checkboxContainer = $('#checkboxContainer'); - if (authType === 'oauth2') { + if (authType === 'sso') { passwordField.removeAttr('required'); passwordField.prop('disabled', true).val(''); passwordFieldContainer.slideUp('fast'); diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index ac94030a4..14e060e4c 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -59,7 +59,7 @@ - + diff --git a/src/main/resources/templates/misc/compare.html b/src/main/resources/templates/misc/compare.html index 9d8cbc766..27a7ed9ed 100644 --- a/src/main/resources/templates/misc/compare.html +++ b/src/main/resources/templates/misc/compare.html @@ -156,7 +156,7 @@ resultDiv2.innerHTML = loading; // Create a new Worker - const worker = new Worker('/js/compare/pdfWorker.js'); + const worker = new Worker('./js/compare/pdfWorker.js'); // Post messages to the worker