diff --git a/build.gradle b/build.gradle index 6ab3a70d3..0a0b36cd7 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,12 @@ java { repositories { mavenCentral() maven { url "https://jitpack.io" } + maven { + url "https://build.shibboleth.net/nexus/content/repositories/releases/" + } + maven { + url "https://build.shibboleth.net/maven/releases/" + } } licenseReport { @@ -114,6 +120,10 @@ configurations.all { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" } dependencies { + + + + //security updates implementation "org.springframework:spring-webmvc:6.1.9" @@ -128,7 +138,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion" - if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { +if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" runtimeOnly "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" @@ -137,7 +147,8 @@ dependencies { //2.2.x requires rebuild of DB file.. need migration path runtimeOnly "com.h2database:h2:2.1.214" // implementation "com.h2database:h2:2.2.224" - } + implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.3' + } testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion" 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 3d6411bc7..2cd28d80c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -6,37 +6,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.Lazy; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.Customizer; 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; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -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.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutSuccessHandler; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutSuccessHandler; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestValidator; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestValidator; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseValidator; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseValidator; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; @@ -47,19 +30,11 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationF import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; -import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler; import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler; -import stirling.software.SPDF.config.security.saml.SAMLUserDetailsService; -import stirling.software.SPDF.config.security.saml.SAMLConfig; +import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler; import stirling.software.SPDF.config.security.saml.SAMLLogoutSuccessHandler; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.model.ApplicationProperties; -import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; -import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; -import stirling.software.SPDF.model.User; -import stirling.software.SPDF.model.provider.GithubProvider; -import stirling.software.SPDF.model.provider.GoogleProvider; -import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.repository.JPATokenRepositoryImpl; @Configuration @@ -69,6 +44,15 @@ public class SecurityConfiguration { @Autowired private CustomUserDetailsService userDetailsService; + @Autowired(required = false) + private GrantedAuthoritiesMapper userAuthoritiesMapper; + + @Autowired(required = false) + private OpenSaml4AuthenticationProvider samlAuthenticationProvider; + + @Autowired(required = false) + private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class); @Bean @@ -153,6 +137,7 @@ public class SecurityConfiguration { return trimmedUri.startsWith("/login") || trimmedUri.startsWith("/oauth") + || trimmedUri.startsWith("/saml2") || trimmedUri.endsWith(".svg") || trimmedUri.startsWith( "/register") @@ -202,7 +187,7 @@ public class SecurityConfiguration { userService, loginAttemptService)) .userAuthoritiesMapper( - userAuthoritiesMapper()))) + userAuthoritiesMapper))) .logout( logout -> logout.logoutSuccessHandler( @@ -220,13 +205,19 @@ public class SecurityConfiguration { http.saml2Login( saml2 -> - saml2.loginPage("/saml2") + saml2.relyingPartyRegistrationRepository( + relyingPartyRegistrationRepository) + .loginProcessingUrl("/login/saml2/sso/stirling") + .loginPage("/saml2") + .authenticationManager( + new ProviderManager( + samlAuthenticationProvider)) .successHandler( new CustomSAMLAuthenticationSuccessHandler( loginAttemptService, userService)) .failureHandler( - new CustomSAMLAuthenticationFailureHandler()) - .userDetailsService(new SAMLUserDetailsService())) + new CustomSAMLAuthenticationFailureHandler())) + .saml2Metadata(Customizer.withDefaults()) .logout( logout -> logout.logoutSuccessHandler( @@ -240,178 +231,6 @@ public class SecurityConfiguration { return http.build(); } - // Client Registration Repository for OAUTH2 OIDC Login - @Bean - @ConditionalOnProperty( - value = "security.oauth2.enabled", - havingValue = "true", - matchIfMissing = false) - public ClientRegistrationRepository clientRegistrationRepository() { - List registrations = new ArrayList<>(); - - githubClientRegistration().ifPresent(registrations::add); - oidcClientRegistration().ifPresent(registrations::add); - googleClientRegistration().ifPresent(registrations::add); - keycloakClientRegistration().ifPresent(registrations::add); - - if (registrations.isEmpty()) { - logger.error("At least one OAuth2 provider must be configured"); - System.exit(1); - } - - return new InMemoryClientRegistrationRepository(registrations); - } - - private Optional googleClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); - if (oauth == null || !oauth.getEnabled()) { - return Optional.empty(); - } - Client client = oauth.getClient(); - if (client == null) { - return Optional.empty(); - } - GoogleProvider google = client.getGoogle(); - return google != null && google.isSettingsValid() - ? Optional.of( - ClientRegistration.withRegistrationId(google.getName()) - .clientId(google.getClientId()) - .clientSecret(google.getClientSecret()) - .scope(google.getScopes()) - .authorizationUri(google.getAuthorizationuri()) - .tokenUri(google.getTokenuri()) - .userInfoUri(google.getUserinfouri()) - .userNameAttributeName(google.getUseAsUsername()) - .clientName(google.getClientName()) - .redirectUri("{baseUrl}/login/oauth2/code/" + google.getName()) - .authorizationGrantType( - org.springframework.security.oauth2.core - .AuthorizationGrantType.AUTHORIZATION_CODE) - .build()) - : Optional.empty(); - } - - private Optional keycloakClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); - if (oauth == null || !oauth.getEnabled()) { - return Optional.empty(); - } - Client client = oauth.getClient(); - if (client == null) { - return Optional.empty(); - } - KeycloakProvider keycloak = client.getKeycloak(); - - return keycloak != null && keycloak.isSettingsValid() - ? Optional.of( - ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) - .registrationId(keycloak.getName()) - .clientId(keycloak.getClientId()) - .clientSecret(keycloak.getClientSecret()) - .scope(keycloak.getScopes()) - .userNameAttributeName(keycloak.getUseAsUsername()) - .clientName(keycloak.getClientName()) - .build()) - : Optional.empty(); - } - - private Optional githubClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); - if (oauth == null || !oauth.getEnabled()) { - return Optional.empty(); - } - Client client = oauth.getClient(); - if (client == null) { - return Optional.empty(); - } - GithubProvider github = client.getGithub(); - return github != null && github.isSettingsValid() - ? Optional.of( - ClientRegistration.withRegistrationId(github.getName()) - .clientId(github.getClientId()) - .clientSecret(github.getClientSecret()) - .scope(github.getScopes()) - .authorizationUri(github.getAuthorizationuri()) - .tokenUri(github.getTokenuri()) - .userInfoUri(github.getUserinfouri()) - .userNameAttributeName(github.getUseAsUsername()) - .clientName(github.getClientName()) - .redirectUri("{baseUrl}/login/oauth2/code/" + github.getName()) - .authorizationGrantType( - org.springframework.security.oauth2.core - .AuthorizationGrantType.AUTHORIZATION_CODE) - .build()) - : Optional.empty(); - } - - private Optional oidcClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); - if (oauth == null - || oauth.getIssuer() == null - || oauth.getIssuer().isEmpty() - || oauth.getClientId() == null - || oauth.getClientId().isEmpty() - || oauth.getClientSecret() == null - || oauth.getClientSecret().isEmpty() - || oauth.getScopes() == null - || oauth.getScopes().isEmpty() - || oauth.getUseAsUsername() == null - || oauth.getUseAsUsername().isEmpty()) { - return Optional.empty(); - } - return Optional.of( - ClientRegistrations.fromIssuerLocation(oauth.getIssuer()) - .registrationId("oidc") - .clientId(oauth.getClientId()) - .clientSecret(oauth.getClientSecret()) - .scope(oauth.getScopes()) - .userNameAttributeName(oauth.getUseAsUsername()) - .clientName("OIDC") - .build()); - } - - /* - This following function is to grant Authorities to the OAUTH2 user from the values stored in the database. - This is required for the internal; 'hasRole()' function to give out the correct role. - */ - @Bean - @ConditionalOnProperty( - value = "security.oauth2.enabled", - havingValue = "true", - matchIfMissing = false) - GrantedAuthoritiesMapper userAuthoritiesMapper() { - return (authorities) -> { - Set mappedAuthorities = new HashSet<>(); - - authorities.forEach( - authority -> { - // Add existing OAUTH2 Authorities - mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); - - // Add Authorities from database for existing user, if user is present. - if (authority instanceof OAuth2UserAuthority oauth2Auth) { - String useAsUsername = - applicationProperties - .getSecurity() - .getOAUTH2() - .getUseAsUsername(); - Optional userOpt = - userService.findByUsernameIgnoreCase( - (String) oauth2Auth.getAttributes().get(useAsUsername)); - if (userOpt.isPresent()) { - User user = userOpt.get(); - if (user != null) { - mappedAuthorities.add( - new SimpleGrantedAuthority( - userService.findRole(user).getAuthority())); - } - } - } - }); - return mappedAuthorities; - }; - } - @Bean public IPRateLimitingFilter rateLimitingFilter() { int maxRequestsPerIp = 1000000; // Example limit TODO add config level @@ -427,42 +246,4 @@ public class SecurityConfiguration { public boolean activSecurity() { return true; } - - // SAML Configuration - @Bean - public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { - RelyingPartyRegistration registration = RelyingPartyRegistration - .withRegistrationId("saml") - .entityId(applicationProperties.getSecurity().getSAML().getEntityId()) - .assertionConsumerServiceLocation(applicationProperties.getSecurity().getSAML().getSpBaseUrl() + "/saml2/acs") - .singleLogoutServiceLocation(applicationProperties.getSecurity().getSAML().getSpBaseUrl() + "/saml2/logout") - .idpWebSsoUrl(applicationProperties.getSecurity().getSAML().getIdpMetadataLocation()) - .build(); - return new InMemoryRelyingPartyRegistrationRepository(registration); - } - - @Bean - public Saml2LogoutRequestRepository logoutRequestRepository() { - return new OpenSaml4LogoutRequestRepository(); - } - - @Bean - public Saml2LogoutResponseRepository logoutResponseRepository() { - return new OpenSaml4LogoutResponseRepository(); - } - - @Bean - public Saml2LogoutSuccessHandler logoutSuccessHandler() { - return new OpenSaml4LogoutSuccessHandler(); - } - - @Bean - public Saml2LogoutRequestValidator logoutRequestValidator() { - return new OpenSaml4LogoutRequestValidator(); - } - - @Bean - public Saml2LogoutResponseValidator logoutResponseValidator() { - return new OpenSaml4LogoutResponseValidator(); - } } 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 78f178222..5adfdbc76 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -1,5 +1,9 @@ package stirling.software.SPDF.config.security; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; @@ -13,6 +17,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 stirling.software.SPDF.config.DatabaseBackupInterface; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; @@ -23,10 +28,6 @@ import stirling.software.SPDF.model.User; import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.UserRepository; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; - @Service public class UserService implements UserServiceInterface { diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Config.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Config.java new file mode 100644 index 000000000..bb28a86f9 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Config.java @@ -0,0 +1,211 @@ +package stirling.software.SPDF.config.security.oauth2; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +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 lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; +import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.model.provider.GithubProvider; +import stirling.software.SPDF.model.provider.GoogleProvider; +import stirling.software.SPDF.model.provider.KeycloakProvider; + +@Configuration +@Slf4j +public class OAuth2Config { + @Autowired ApplicationProperties applicationProperties; + + @Autowired @Lazy private UserService userService; + + // Client Registration Repository for OAUTH2 OIDC Login + @Bean + @ConditionalOnProperty( + value = "security.oauth2.enabled", + havingValue = "true", + matchIfMissing = false) + public ClientRegistrationRepository clientRegistrationRepository() { + List registrations = new ArrayList<>(); + + githubClientRegistration().ifPresent(registrations::add); + oidcClientRegistration().ifPresent(registrations::add); + googleClientRegistration().ifPresent(registrations::add); + keycloakClientRegistration().ifPresent(registrations::add); + + if (registrations.isEmpty()) { + log.error("At least one OAuth2 provider must be configured"); + System.exit(1); + } + + return new InMemoryClientRegistrationRepository(registrations); + } + + private Optional googleClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); + if (oauth == null || !oauth.getEnabled()) { + return Optional.empty(); + } + Client client = oauth.getClient(); + if (client == null) { + return Optional.empty(); + } + GoogleProvider google = client.getGoogle(); + return google != null && google.isSettingsValid() + ? Optional.of( + ClientRegistration.withRegistrationId(google.getName()) + .clientId(google.getClientId()) + .clientSecret(google.getClientSecret()) + .scope(google.getScopes()) + .authorizationUri(google.getAuthorizationuri()) + .tokenUri(google.getTokenuri()) + .userInfoUri(google.getUserinfouri()) + .userNameAttributeName(google.getUseAsUsername()) + .clientName(google.getClientName()) + .redirectUri("{baseUrl}/login/oauth2/code/" + google.getName()) + .authorizationGrantType( + org.springframework.security.oauth2.core + .AuthorizationGrantType.AUTHORIZATION_CODE) + .build()) + : Optional.empty(); + } + + private Optional keycloakClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); + if (oauth == null || !oauth.getEnabled()) { + return Optional.empty(); + } + Client client = oauth.getClient(); + if (client == null) { + return Optional.empty(); + } + KeycloakProvider keycloak = client.getKeycloak(); + + return keycloak != null && keycloak.isSettingsValid() + ? Optional.of( + ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) + .registrationId(keycloak.getName()) + .clientId(keycloak.getClientId()) + .clientSecret(keycloak.getClientSecret()) + .scope(keycloak.getScopes()) + .userNameAttributeName(keycloak.getUseAsUsername()) + .clientName(keycloak.getClientName()) + .build()) + : Optional.empty(); + } + + private Optional githubClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); + if (oauth == null || !oauth.getEnabled()) { + return Optional.empty(); + } + Client client = oauth.getClient(); + if (client == null) { + return Optional.empty(); + } + GithubProvider github = client.getGithub(); + return github != null && github.isSettingsValid() + ? Optional.of( + ClientRegistration.withRegistrationId(github.getName()) + .clientId(github.getClientId()) + .clientSecret(github.getClientSecret()) + .scope(github.getScopes()) + .authorizationUri(github.getAuthorizationuri()) + .tokenUri(github.getTokenuri()) + .userInfoUri(github.getUserinfouri()) + .userNameAttributeName(github.getUseAsUsername()) + .clientName(github.getClientName()) + .redirectUri("{baseUrl}/login/oauth2/code/" + github.getName()) + .authorizationGrantType( + org.springframework.security.oauth2.core + .AuthorizationGrantType.AUTHORIZATION_CODE) + .build()) + : Optional.empty(); + } + + private Optional oidcClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); + if (oauth == null + || oauth.getIssuer() == null + || oauth.getIssuer().isEmpty() + || oauth.getClientId() == null + || oauth.getClientId().isEmpty() + || oauth.getClientSecret() == null + || oauth.getClientSecret().isEmpty() + || oauth.getScopes() == null + || oauth.getScopes().isEmpty() + || oauth.getUseAsUsername() == null + || oauth.getUseAsUsername().isEmpty()) { + return Optional.empty(); + } + return Optional.of( + ClientRegistrations.fromIssuerLocation(oauth.getIssuer()) + .registrationId("oidc") + .clientId(oauth.getClientId()) + .clientSecret(oauth.getClientSecret()) + .scope(oauth.getScopes()) + .userNameAttributeName(oauth.getUseAsUsername()) + .clientName("OIDC") + .build()); + } + + /* + This following function is to grant Authorities to the OAUTH2 user from the values stored in the database. + This is required for the internal; 'hasRole()' function to give out the correct role. + */ + @Bean + @ConditionalOnProperty( + value = "security.oauth2.enabled", + havingValue = "true", + matchIfMissing = false) + GrantedAuthoritiesMapper userAuthoritiesMapper() { + return (authorities) -> { + Set mappedAuthorities = new HashSet<>(); + + authorities.forEach( + authority -> { + // Add existing OAUTH2 Authorities + mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); + + // Add Authorities from database for existing user, if user is present. + if (authority instanceof OAuth2UserAuthority oauth2Auth) { + String useAsUsername = + applicationProperties + .getSecurity() + .getOAUTH2() + .getUseAsUsername(); + Optional userOpt = + userService.findByUsernameIgnoreCase( + (String) oauth2Auth.getAttributes().get(useAsUsername)); + if (userOpt.isPresent()) { + User user = userOpt.get(); + if (user != null) { + mappedAuthorities.add( + new SimpleGrantedAuthority( + userService.findRole(user).getAuthority())); + } + } + } + }); + return mappedAuthorities; + }; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationFailureHandler.java index 13bfa200a..2a61e771f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationFailureHandler.java @@ -41,7 +41,8 @@ public class CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticat } if (exception instanceof Saml2AuthenticationException) { log.error("SAML2 Authentication error: ", exception); - getRedirectStrategy().sendRedirect(request, response, "/logout?error=saml2AuthenticationError"); + getRedirectStrategy() + .sendRedirect(request, response, "/logout?error=saml2AuthenticationError"); return; } log.error("Unhandled authentication exception", exception); diff --git a/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationSuccessHandler.java index b211a100d..b991e52aa 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml/CustomSAMLAuthenticationSuccessHandler.java @@ -11,21 +11,26 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.security.LoginAttemptService; +import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.utils.RequestUriUtils; @Slf4j -public class CustomSAMLAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { +public class CustomSAMLAuthenticationSuccessHandler + extends SavedRequestAwareAuthenticationSuccessHandler { private LoginAttemptService loginAttemptService; private UserService userService; - public CustomSAMLAuthenticationSuccessHandler(LoginAttemptService loginAttemptService, UserService userService) { + public CustomSAMLAuthenticationSuccessHandler( + LoginAttemptService loginAttemptService, UserService userService) { this.loginAttemptService = loginAttemptService; this.userService = userService; } @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + public void onAuthenticationSuccess( + HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { String userName = request.getParameter("username"); @@ -37,9 +42,14 @@ public class CustomSAMLAuthenticationSuccessHandler extends SavedRequestAwareAut // Get the saved request HttpSession session = request.getSession(false); - SavedRequest savedRequest = (session != null) ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null; + SavedRequest savedRequest = + (session != null) + ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") + : null; - if (savedRequest != null && !RequestUriUtils.isStaticResource(request.getContextPath(), savedRequest.getRedirectUrl())) { + if (savedRequest != null + && !RequestUriUtils.isStaticResource( + request.getContextPath(), savedRequest.getRedirectUrl())) { // Redirect to the original destination super.onAuthenticationSuccess(request, response, authentication); } else { diff --git a/src/main/java/stirling/software/SPDF/config/security/saml/SAMLConfig.java b/src/main/java/stirling/software/SPDF/config/security/saml/SAMLConfig.java deleted file mode 100644 index d3e8f0808..000000000 --- a/src/main/java/stirling/software/SPDF/config/security/saml/SAMLConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -package stirling.software.SPDF.config.security.saml; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseRepository; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutSuccessHandler; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutSuccessHandler; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestValidator; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestValidator; -import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseValidator; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseValidator; - -import stirling.software.SPDF.model.ApplicationProperties; - -@Configuration -public class SAMLConfig { - - private final ApplicationProperties applicationProperties; - - public SAMLConfig(ApplicationProperties applicationProperties) { - this.applicationProperties = applicationProperties; - } - - @Bean - public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { - RelyingPartyRegistration registration = RelyingPartyRegistration - .withRegistrationId("saml") - .entityId(applicationProperties.getSecurity().getSAML().getEntityId()) - .assertionConsumerServiceLocation(applicationProperties.getSecurity().getSAML().getSpBaseUrl() + "/saml2/acs") - .singleLogoutServiceLocation(applicationProperties.getSecurity().getSAML().getSpBaseUrl() + "/saml2/logout") - .idpWebSsoUrl(applicationProperties.getSecurity().getSAML().getIdpMetadataLocation()) - .build(); - return new InMemoryRelyingPartyRegistrationRepository(registration); - } - - @Bean - public Saml2LogoutRequestRepository logoutRequestRepository() { - return new OpenSaml4LogoutRequestRepository(); - } - - @Bean - public Saml2LogoutResponseRepository logoutResponseRepository() { - return new OpenSaml4LogoutResponseRepository(); - } - - @Bean - public Saml2LogoutSuccessHandler logoutSuccessHandler() { - return new OpenSaml4LogoutSuccessHandler(); - } - - @Bean - public Saml2LogoutRequestValidator logoutRequestValidator() { - return new OpenSaml4LogoutRequestValidator(); - } - - @Bean - public Saml2LogoutResponseValidator logoutResponseValidator() { - return new OpenSaml4LogoutResponseValidator(); - } -} diff --git a/src/main/java/stirling/software/SPDF/config/security/saml/SAMLLogoutSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml/SAMLLogoutSuccessHandler.java index c236fc4e5..24e81889f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml/SAMLLogoutSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml/SAMLLogoutSuccessHandler.java @@ -29,7 +29,9 @@ public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { } protected String determineTargetUrl( - HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) { // Default to the root URL return "/"; } diff --git a/src/main/java/stirling/software/SPDF/config/security/saml/SAMLUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/saml/SAMLUserDetailsService.java deleted file mode 100644 index efa80637e..000000000 --- a/src/main/java/stirling/software/SPDF/config/security/saml/SAMLUserDetailsService.java +++ /dev/null @@ -1,38 +0,0 @@ -package stirling.software.SPDF.config.security.saml; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; -import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; -import org.springframework.stereotype.Service; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class SAMLUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - throw new UnsupportedOperationException("This method is not supported for SAML authentication"); - } - - public UserDetails loadUserBySAML(Saml2Authentication authentication) { - Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); - String username = principal.getName(); - Collection authorities = extractAuthorities(principal); - - return new org.springframework.security.core.userdetails.User(username, "", authorities); - } - - private Collection extractAuthorities(Saml2AuthenticatedPrincipal principal) { - List roles = principal.getAttribute("roles"); - return roles.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/stirling/software/SPDF/config/security/saml/SamlConfig.java b/src/main/java/stirling/software/SPDF/config/security/saml/SamlConfig.java new file mode 100644 index 000000000..9b6eab2cb --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/saml/SamlConfig.java @@ -0,0 +1,216 @@ +package stirling.software.SPDF.config.security.saml; + +import java.io.InputStream; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.*; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; +import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter; + +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.ApplicationProperties; + +@Configuration +@Slf4j +public class SamlConfig { + + @Autowired ApplicationProperties applicationProperties; + + @Bean + public OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider() { + OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + provider.setResponseAuthenticationConverter( + responseToken -> { + Saml2AuthenticationToken token = responseToken.getToken(); + log.info("Received SAML response: {}", token.getSaml2Response()); + // Your custom conversion logic here + // For now, we'll just return the token as is + return token; + }); + return provider; + } + + @Bean + @ConditionalOnProperty( + value = "security.saml.enabled", + havingValue = "true", + matchIfMissing = false) + public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + RelyingPartyRegistration registration = + RelyingPartyRegistration.withRegistrationId( + applicationProperties.getSecurity().getSAML().getRegistrationId()) + .entityId(applicationProperties.getSecurity().getSAML().getEntityId()) + .assertionConsumerServiceLocation( + applicationProperties.getSecurity().getSAML().getSpBaseUrl() + + "/login/saml2/sso/stirling") + .singleLogoutServiceLocation( + applicationProperties.getSecurity().getSAML().getSpBaseUrl() + + "/logout/saml2/slo") + .singleLogoutServiceResponseLocation( + applicationProperties.getSecurity().getSAML().getSpBaseUrl() + + "/logout/saml2/slo") + .signingX509Credentials(credentials -> credentials.add(signingCredential())) + .assertingPartyDetails( + party -> + party.entityId( + applicationProperties + .getSecurity() + .getSAML() + .getEntityId()) + .singleSignOnServiceLocation( + applicationProperties + .getSecurity() + .getSAML() + .getIdpMetadataLocation()) + .wantAuthnRequestsSigned(true) + .verificationX509Credentials( + c -> c.add(this.realmCertificate()))) + .build(); + return new InMemoryRelyingPartyRegistrationRepository(registration); + } + + private Saml2X509Credential signingCredential() { + log.info("Starting to load signing credential"); + try { + Resource storeResource = + applicationProperties + .getSecurity() + .getSAML() + .getKeystore() + .getKeystoreResource(); + log.info("Keystore resource: {}", storeResource.getDescription()); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + try (InputStream is = storeResource.getInputStream()) { + keyStore.load( + is, + applicationProperties + .getSecurity() + .getSAML() + .getKeystore() + .getKeystorePassword() + .toCharArray()); + log.info("Keystore loaded successfully"); + } + + String keyAlias = + applicationProperties.getSecurity().getSAML().getKeystore().getKeyAlias(); + log.info("Attempting to retrieve private key with alias: {}", keyAlias); + + PrivateKey privateKey = + (PrivateKey) + keyStore.getKey( + keyAlias, + applicationProperties + .getSecurity() + .getSAML() + .getKeystore() + .getKeyPassword() + .toCharArray()); + + if (privateKey == null) { + log.error("Private key not found for alias: {}", keyAlias); + throw new RuntimeException("Private key not found in keystore"); + } + + log.info("Private key retrieved successfully"); + + X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyAlias); + + if (certificate == null) { + log.info("Certificate not found for alias: {}", keyAlias); + throw new RuntimeException("Certificate not found in keystore"); + } + + log.info( + "Certificate retrieved successfully. Subject: {}", + certificate.getSubjectX500Principal()); + + log.info("Signing credential created successfully"); + return Saml2X509Credential.signing(privateKey, certificate); + } catch (Exception e) { + log.error("Error loading signing credential", e); + throw new RuntimeException("Error loading signing credential", e); + } + } + + private Saml2X509Credential realmCertificate() { + log.info("Starting to load realm certificate"); + try { + Resource storeResource = + applicationProperties + .getSecurity() + .getSAML() + .getKeystore() + .getKeystoreResource(); + log.info("Keystore resource: {}", storeResource.getDescription()); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + try (InputStream is = storeResource.getInputStream()) { + keyStore.load( + is, + applicationProperties + .getSecurity() + .getSAML() + .getKeystore() + .getKeystorePassword() + .toCharArray()); + log.info("Keystore loaded successfully"); + } + + String realmCertificateAlias = + applicationProperties + .getSecurity() + .getSAML() + .getKeystore() + .getRealmCertificateAlias(); + log.info( + "Attempting to retrieve realm certificate with alias: {}", + realmCertificateAlias); + + X509Certificate certificate = + (X509Certificate) keyStore.getCertificate(realmCertificateAlias); + + if (certificate == null) { + log.error("Realm certificate not found for alias: {}", realmCertificateAlias); + throw new RuntimeException("Realm certificate not found in keystore"); + } + + log.info( + "Realm certificate retrieved successfully. Subject: {}", + certificate.getSubjectX500Principal()); + + log.info("Realm certificate credential created successfully"); + return Saml2X509Credential.verification(certificate); + } catch (Exception e) { + log.error("Error loading realm certificate", e); + throw new RuntimeException("Error loading realm certificate", e); + } + } + + @Bean + @ConditionalOnProperty( + value = "security.saml.enabled", + havingValue = "true", + matchIfMissing = false) + public Saml2MetadataFilter metadataFilter(RelyingPartyRegistrationRepository registrations) { + DefaultRelyingPartyRegistrationResolver registrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations); + OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver(); + return new Saml2MetadataFilter(registrationResolver, metadataResolver); + } +} 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 2f2ed8f49..dad24b632 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -1,10 +1,10 @@ package stirling.software.SPDF.controller.web; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; @@ -13,6 +13,14 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.model.*; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; @@ -22,11 +30,6 @@ import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.repository.UserRepository; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.stream.Collectors; - @Controller @Slf4j @Tag(name = "Account Security", description = "Account Security APIs") diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 266d4520f..4e5afd11a 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.model; +import java.security.KeyStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -11,7 +12,11 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import lombok.Data; import stirling.software.SPDF.config.YamlPropertySourceFactory; import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GoogleProvider; @@ -130,6 +135,7 @@ public class ApplicationProperties { private Boolean csrfDisabled; private InitialLogin initialLogin; private OAUTH2 oauth2; + private SAML saml; private int loginAttemptCount; private long loginResetTimeMinutes; private String loginMethod = "all"; @@ -174,6 +180,14 @@ public class ApplicationProperties { this.oauth2 = oauth2; } + public SAML getSAML() { + return saml != null ? saml : new SAML(); + } + + public void setSAML(SAML saml) { + this.saml = saml; + } + public Boolean getEnableLogin() { return enableLogin; } @@ -235,6 +249,34 @@ public class ApplicationProperties { } } + @Data + public static class SAML { + private Boolean enabled = false; + private String entityId; + private String registrationId; + private String spBaseUrl; + private String idpMetadataLocation; + private KeyStore keystore; + + @Data + public static class KeyStore { + private String keystoreLocation; + private String keystorePassword; + private String keyAlias; + private String keyPassword; + private String realmCertificateAlias; + + public Resource getKeystoreResource() { + if (keystoreLocation.startsWith("classpath:")) { + return new ClassPathResource( + keystoreLocation.substring("classpath:".length())); + } else { + return new FileSystemResource(keystoreLocation); + } + } + } + } + public static class OAUTH2 { private Boolean enabled = false; private String issuer; diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index 7006712ba..ddfb71353 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.model; -import jakarta.persistence.*; - import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; @@ -9,6 +7,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import jakarta.persistence.*; + @Entity @Table(name = "users") public class User implements Serializable { diff --git a/src/main/java/stirling/software/SPDF/repository/UserRepository.java b/src/main/java/stirling/software/SPDF/repository/UserRepository.java index a80b58cb7..3da21634f 100644 --- a/src/main/java/stirling/software/SPDF/repository/UserRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/UserRepository.java @@ -1,11 +1,12 @@ package stirling.software.SPDF.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import stirling.software.SPDF.model.User; -import java.util.Optional; +import stirling.software.SPDF.model.User; @Repository public interface UserRepository extends JpaRepository { diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 480badcbe..21d921c83 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -262,5 +262,4 @@ public class GeneralUtils { } return true; } - } diff --git a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java index 865f72a12..b3e02b038 100644 --- a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java @@ -4,16 +4,7 @@ public class RequestUriUtils { public static boolean isStaticResource(String requestURI) { - return requestURI.startsWith("/css/") - || requestURI.startsWith("/fonts/") - || requestURI.startsWith("/js/") - || requestURI.startsWith("/images/") - || requestURI.startsWith("/public/") - || requestURI.startsWith("/pdfjs/") - || requestURI.startsWith("/pdfjs-legacy/") - || requestURI.endsWith(".svg") - || requestURI.endsWith(".webmanifest") - || requestURI.startsWith("/api/v1/info/status"); + return isStaticResource("", requestURI); } public static boolean isStaticResource(String contextPath, String requestURI) { @@ -24,6 +15,7 @@ public class RequestUriUtils { || requestURI.startsWith(contextPath + "/images/") || requestURI.startsWith(contextPath + "/public/") || requestURI.startsWith(contextPath + "/pdfjs/") + || requestURI.startsWith(contextPath + "/saml2") || requestURI.endsWith(".svg") || requestURI.endsWith(".webmanifest") || requestURI.startsWith(contextPath + "/api/v1/info/status"); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cae1dce3d..e947a5d59 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,12 @@ multipart.enabled=true +otel.metrics.exporter=prometheus +otel.exporter.prometheus.port=9464 +otel.service.name=stirling-pdf + +logging.level.org.springframework.security.saml2=DEBUG +logging.level.org.springframework.security=DEBUG + logging.level.org.springframework=WARN logging.level.org.hibernate=WARN logging.level.org.eclipse.jetty=WARN diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index d6476fffe..28a79646e 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -49,9 +49,16 @@ security: provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' saml: enabled: false # set to 'true' to enable SAML login (Note: enableLogin must also be 'true' for this to work) + registrationId: stirling entityId: '' # Entity ID for the Service Provider (SP) idpMetadataLocation: '' # URL or file path to the Identity Provider (IdP) metadata spBaseUrl: '' # Base URL for the Service Provider (SP) + keystore: + keystoreLocation: /config/keystore.jks + keystorePassword: stirlingstore + keyAlias: stirling + keyPassword: stirlingkey + realmCertificateAlias: master system: defaultLocale: en-US # Set the default language (e.g. 'de-DE', 'fr-FR', etc) diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 7105fb94c..0c36c22cd 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -114,7 +114,7 @@ favicon

Stirling-PDF

-
+
Login Via SSO

@@ -184,7 +184,7 @@ OpenID Connect
- SAML + SAML