From a875ae303455bb48015874f5bb521ede2040be6e Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.>
Date: Wed, 4 Sep 2024 15:20:44 +0100
Subject: [PATCH] saml test

---
 build.gradle                                  |  15 +-
 .../security/SecurityConfiguration.java       | 267 ++----------------
 .../SPDF/config/security/UserService.java     |   9 +-
 .../config/security/oauth2/OAuth2Config.java  | 211 ++++++++++++++
 ...ustomSAMLAuthenticationFailureHandler.java |   3 +-
 ...ustomSAMLAuthenticationSuccessHandler.java |  20 +-
 .../SPDF/config/security/saml/SAMLConfig.java |  66 -----
 .../saml/SAMLLogoutSuccessHandler.java        |   4 +-
 .../security/saml/SAMLUserDetailsService.java |  38 ---
 .../SPDF/config/security/saml/SamlConfig.java | 216 ++++++++++++++
 .../controller/web/AccountWebController.java  |  23 +-
 .../SPDF/model/ApplicationProperties.java     |  42 +++
 .../stirling/software/SPDF/model/User.java    |   4 +-
 .../SPDF/repository/UserRepository.java       |   5 +-
 .../software/SPDF/utils/GeneralUtils.java     |   1 -
 .../software/SPDF/utils/RequestUriUtils.java  |  12 +-
 src/main/resources/application.properties     |   7 +
 src/main/resources/settings.yml.template      |   7 +
 src/main/resources/templates/login.html       |   4 +-
 19 files changed, 567 insertions(+), 387 deletions(-)
 create mode 100644 src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Config.java
 delete mode 100644 src/main/java/stirling/software/SPDF/config/security/saml/SAMLConfig.java
 delete mode 100644 src/main/java/stirling/software/SPDF/config/security/saml/SAMLUserDetailsService.java
 create mode 100644 src/main/java/stirling/software/SPDF/config/security/saml/SamlConfig.java

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<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();
-    }
 }
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<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;
+        };
+    }
+}
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<? 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());
-    }
-}
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<User, Long> {
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 @@
           <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">