mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-22 15:35:03 +00:00
saml test
This commit is contained in:
parent
2989d8d416
commit
a875ae3034
15
build.gradle
15
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"
|
||||
|
||||
|
@ -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<ClientRegistration> 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<ClientRegistration> 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<ClientRegistration> 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<ClientRegistration> 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<ClientRegistration> 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<GrantedAuthority> 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<User> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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<ClientRegistration> 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<ClientRegistration> 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<ClientRegistration> 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<ClientRegistration> 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<ClientRegistration> 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<GrantedAuthority> 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<User> 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;
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 "/";
|
||||
}
|
||||
|
@ -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<? extends GrantedAuthority> authorities = extractAuthorities(principal);
|
||||
|
||||
return new org.springframework.security.core.userdetails.User(username, "", authorities);
|
||||
}
|
||||
|
||||
private Collection<? extends GrantedAuthority> extractAuthorities(Saml2AuthenticatedPrincipal principal) {
|
||||
List<String> roles = principal.getAttribute("roles");
|
||||
return roles.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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<User, Long> {
|
||||
|
@ -262,5 +262,4 @@ public class GeneralUtils {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -114,7 +114,7 @@
|
||||
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
|
||||
|
||||
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
|
||||
<div th:if="${oAuth2Enabled} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')">
|
||||
<div th:if="(${oAuth2Enabled} or ${samlEnabled}) and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')">
|
||||
<a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
|
||||
<br>
|
||||
<br>
|
||||
@ -184,7 +184,7 @@
|
||||
<a th:href="@{|/oauth2/authorization/${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">OpenID Connect</a>
|
||||
</div>
|
||||
<div class="mb-3" th:if="${samlEnabled}">
|
||||
<a th:href="@{'/saml2/authenticate/saml'}" class="w-100 btn btn-lg btn-primary" th:text="#{login.samlSignIn}">SAML</a>
|
||||
<a th:href="@{'/saml2/authenticate/stirling'}" class="w-100 btn btn-lg btn-primary" th:text="#{login.ssoSignIn}">SAML</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
Loading…
x
Reference in New Issue
Block a user