From 264de0bbd7dd7480cbecc7d546e286ed466141a1 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Wed, 19 Feb 2025 16:22:15 +0000 Subject: [PATCH] Added SSOAutoLogin functionality to SAML 2 logins --- .../SPDF/EE/KeygenLicenseVerifier.java | 8 ++-- .../security/SecurityConfiguration.java | 21 ++-------- .../controller/web/AccountWebController.java | 7 +++- .../SPDF/model/ApplicationProperties.java | 2 +- src/main/resources/settings.yml.template | 39 +++++++++---------- 5 files changed, 33 insertions(+), 44 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java index c75c98e9..504f1d63 100644 --- a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java +++ b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java @@ -19,6 +19,7 @@ import stirling.software.SPDF.utils.GeneralUtils; @Service @Slf4j public class KeygenLicenseVerifier { + // todo: place in config files? private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372"; private static final String BASE_URL = "https://api.keygen.sh/v1/accounts"; private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -67,7 +68,7 @@ public class KeygenLicenseVerifier { return false; } catch (Exception e) { - log.error("Error verifying license: " + e.getMessage()); + log.error("Error verifying license: {}", e.getMessage()); return false; } } @@ -94,10 +95,9 @@ public class KeygenLicenseVerifier { .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - log.debug(" validateLicenseResponse body: " + response.body()); + log.debug("ValidateLicenseResponse body: {}", response.body()); JsonNode jsonResponse = objectMapper.readTree(response.body()); if (response.statusCode() == 200) { - JsonNode metaNode = jsonResponse.path("meta"); boolean isValid = metaNode.path("valid").asBoolean(); @@ -119,7 +119,7 @@ public class KeygenLicenseVerifier { log.info(applicationProperties.toString()); } else { - log.error("Error validating license. Status code: " + response.statusCode()); + log.error("Error validating license. Status code: {}", response.statusCode()); } return jsonResponse; } 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 a2ee983a..edd56f93 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -1,7 +1,7 @@ package stirling.software.SPDF.config.security; -import java.util.*; - +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -10,7 +10,6 @@ import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -23,16 +22,10 @@ import org.springframework.security.saml2.provider.service.web.authentication.Op 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.DelegatingSecurityContextRepository; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; -import org.springframework.security.web.context.SecurityContextRepository; 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 lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; @@ -47,7 +40,6 @@ import stirling.software.SPDF.repository.PersistentLoginRepository; @Configuration @EnableWebSecurity -@EnableMethodSecurity @Slf4j @DependsOn("runningEE") public class SecurityConfiguration { @@ -104,7 +96,7 @@ public class SecurityConfiguration { } @Bean - public SecurityFilterChain filterChain(HttpSecurity http, SecurityContextRepository securityContextRepository) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { http.csrf(csrf -> csrf.disable()); } @@ -264,13 +256,6 @@ public class SecurityConfiguration { authenticationProvider.setResponseAuthenticationConverter( new CustomSaml2ResponseAuthenticationConverter(userService)); http.authenticationProvider(authenticationProvider) - .securityContext(security -> - security.securityContextRepository( - new DelegatingSecurityContextRepository( - new RequestAttributeSecurityContextRepository(), - new HttpSessionSecurityContextRepository()) - ) - ) .saml2Login( saml2 -> { try { diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index f2dadeb9..f2bbb8ab 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.model.ApplicationProperties; @@ -121,7 +122,11 @@ public class AccountWebController { String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId(); if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) { - return "redirect:login" + saml2AuthenticationPath; + return "redirect:" + + SPDFApplication.getStaticBaseUrl() + + ":" + + SPDFApplication.getStaticPort() + + saml2AuthenticationPath; } else { providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)"); } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 8dc4ccbe..ce9798ec 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -108,7 +108,7 @@ public class ApplicationProperties { private int loginAttemptCount; private long loginResetTimeMinutes; private String loginMethod = "all"; - private String customGlobalAPIKey; // todo: expose? + private String customGlobalAPIKey; public Boolean isAltLogin() { return saml2.getEnabled() || oauth2.getEnabled(); diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index d22863d5..4f465236 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -12,7 +12,7 @@ security: - enableLogin: true # set to 'true' to enable login + enableLogin: false # set to 'true' to enable login csrfDisabled: false # 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 @@ -39,33 +39,32 @@ security: clientSecret: '' # client secret for GitHub OAuth2 scopes: read:user # scope for GitHub OAuth2 useAsUsername: login # field to use as the username for GitHub OAuth2. Available options are: [email | login | name] - issuer: https://trial-6373896.okta.com/home/okta_flow_sso/0oaok4lk1nVvNBnqK697/alnbibn6b0OPFATt20g7 # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint - clientId: 0oaok4lk4eNm6PtFD697 # client ID from your provider - clientSecret: lmwlmxFZSJ0miOoRpUAKf2jg8tVPPXhUxgL2VB-b4uJfhnk4sI02YodKWRX8fLSq # client secret from your provider - logoutUrl: '' + 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: 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: username # default is 'email'; custom fields can be used as the username - scopes: okta.users.read, okta.users.read.self, okta.users.manage.self, okta.groups.read # 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' + 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: true # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true) - provider: okta + enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true) + provider: '' The name of your Provider 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: stirlingpdf-dario-saml - idpMetadataUri: https://trial-6373896.okta.com/app/exkomkf71reALy12X697/sso/saml/metadata # todo: remove - idpSingleLoginUrl: https://trial-6373896.okta.com/app/trial-6373896_stirlingpdfsaml2_1/exkoot0g5ipqOF3Bo697/sso/saml # todo: remove - idpSingleLogoutUrl: https://trial-6373896.okta.com/app/trial-6373896_stirlingpdfsaml2_1/exkoot0g5ipqOF3Bo697/slo/saml # todo: remove - idpIssuer: http://www.okta.com/exkoot0g5ipqOF3Bo697 + registrationId: stirling + idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata + idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml + idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml + idpIssuer: '' idpCert: classpath:okta.cert - privateKey: classpath:private_key.key - spCert: classpath:certificate.crt + privateKey: classpath:saml-private-key.key + spCert: classpath:saml-public-cert.crt enterpriseEdition: - enabled: true # set to 'true' to enable enterprise edition + enabled: false # set to 'true' to enable enterprise edition key: 00000000-0000-0000-0000-000000000000 - SSOAutoLogin: true # Enable to auto login to first provided SSO + SSOAutoLogin: false # Enable to auto login to first provided SSO CustomMetadata: autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username @@ -82,7 +81,7 @@ legal: system: defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc) googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow - enableAlphaFunctionality: true # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes) + enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes) showUpdate: false # see when a new update is available showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true' customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files