mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-04-19 19:21:18 +00:00
This commit is contained in:
parent
c1f78d0f9b
commit
2989d8d416
@ -24,6 +24,19 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||
import org.springframework.security.saml2.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 org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
@ -34,6 +47,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.SAMLLogoutSuccessHandler;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
@ -191,6 +209,29 @@ public class SecurityConfiguration {
|
||||
new CustomOAuth2LogoutSuccessHandler(
|
||||
applicationProperties)));
|
||||
}
|
||||
|
||||
// Handle SAML Logins
|
||||
if (applicationProperties.getSecurity().getSAML() != null
|
||||
&& applicationProperties.getSecurity().getSAML().getEnabled()
|
||||
&& !applicationProperties
|
||||
.getSecurity()
|
||||
.getLoginMethod()
|
||||
.equalsIgnoreCase("normal")) {
|
||||
|
||||
http.saml2Login(
|
||||
saml2 ->
|
||||
saml2.loginPage("/saml2")
|
||||
.successHandler(
|
||||
new CustomSAMLAuthenticationSuccessHandler(
|
||||
loginAttemptService, userService))
|
||||
.failureHandler(
|
||||
new CustomSAMLAuthenticationFailureHandler())
|
||||
.userDetailsService(new SAMLUserDetailsService()))
|
||||
.logout(
|
||||
logout ->
|
||||
logout.logoutSuccessHandler(
|
||||
new SAMLLogoutSuccessHandler()));
|
||||
}
|
||||
} else {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
@ -386,4 +427,42 @@ 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();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
package stirling.software.SPDF.config.security.saml;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
|
||||
if (exception instanceof BadCredentialsException) {
|
||||
log.error("BadCredentialsException", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof DisabledException) {
|
||||
log.error("User is deactivated: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof LockedException) {
|
||||
log.error("Account locked: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof Saml2AuthenticationException) {
|
||||
log.error("SAML2 Authentication error: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
|
||||
return;
|
||||
}
|
||||
log.error("Unhandled authentication exception", exception);
|
||||
super.onAuthenticationFailure(request, response, exception);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package stirling.software.SPDF.config.security.saml;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@Slf4j
|
||||
public class CustomSAMLAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
private UserService userService;
|
||||
|
||||
public CustomSAMLAuthenticationSuccessHandler(LoginAttemptService loginAttemptService, UserService userService) {
|
||||
this.loginAttemptService = loginAttemptService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String userName = request.getParameter("username");
|
||||
if (userService.isUserDisabled(userName)) {
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||
return;
|
||||
}
|
||||
loginAttemptService.loginSucceeded(userName);
|
||||
|
||||
// Get the saved request
|
||||
HttpSession session = request.getSession(false);
|
||||
SavedRequest savedRequest = (session != null) ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null;
|
||||
|
||||
if (savedRequest != null && !RequestUriUtils.isStaticResource(request.getContextPath(), savedRequest.getRedirectUrl())) {
|
||||
// Redirect to the original destination
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
// Redirect to the root URL (considering context path)
|
||||
getRedirectStrategy().sendRedirect(request, response, "/");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package stirling.software.SPDF.config.security.saml;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
@Override
|
||||
public void onLogoutSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException, ServletException {
|
||||
|
||||
String redirectUrl = determineTargetUrl(request, response, authentication);
|
||||
|
||||
if (response.isCommitted()) {
|
||||
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
||||
}
|
||||
|
||||
protected String determineTargetUrl(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
// Default to the root URL
|
||||
return "/";
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
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());
|
||||
}
|
||||
}
|
@ -80,6 +80,8 @@ public class AccountWebController {
|
||||
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
|
||||
model.addAttribute(
|
||||
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||
model.addAttribute(
|
||||
"samlEnabled", applicationProperties.getSecurity().getSAML().getEnabled());
|
||||
|
||||
model.addAttribute("currentPage", "login");
|
||||
|
||||
|
@ -47,6 +47,11 @@ security:
|
||||
useAsUsername: email # Default is 'email'; custom fields can be used as the username
|
||||
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
|
||||
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
||||
saml:
|
||||
enabled: false # set to 'true' to enable SAML login (Note: enableLogin must also be 'true' for this to work)
|
||||
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)
|
||||
|
||||
system:
|
||||
defaultLocale: en-US # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||
|
@ -168,7 +168,7 @@
|
||||
</main>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<div th:if="${oAuth2Enabled}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
|
||||
<div th:if="${oAuth2Enabled or samlEnabled}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -183,6 +183,9 @@
|
||||
<div class="mb-3" th:each="provider : ${providerlist}">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
||||
@ -191,4 +194,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user