spring-mvcspring-securityspring-saml

How to resolve POST Request 405 Error in Spring SAML2 SLO


I'm trying to implement Spring Security SAML2 SSO/SLO and successfully implemented the SSO, but when I tried to follow the Spring security docs for implementing SLO I got this 405 Method Not Allowed Status.

enter image description here

I tried to disable the cors and csrf but I'm still having the same problem, I also tried to exclude the /logout/saml2/slo in filter.

I tried to search this issue but I can't find any info regarding this and all I can find is they are having issue with Saml2 Response.

I also make sure the jks is good and the alias and password is right.

I'm expecting this to succeed and be able to proved SAML2 Response after.

    @Bean
    public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
        if (DomainUtil.isSSOEnabled()) {
            byte[] decodeCertBase64 = Base64.getDecoder().decode(spSigningCertificateBase64.trim());
            char[] password = "REMOVED".toCharArray();
            try (InputStream inputStream = getSamlMetadataInputStream(samlMetadata);
                ByteArrayInputStream certStream = new ByteArrayInputStream(decodeCertBase64);
                FileInputStream fileInputStream = new FileInputStream("/BDO/fedlet/fedletkeystore.jks")) {

                KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                keyStore.load(fileInputStream, password);

                String alias = keyStore.aliases().nextElement();
                RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(alias, password);

                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(certStream);

                Saml2X509Credential signingCredential = Saml2X509Credential.signing(privateKey, cert);

                RelyingPartyRegistration.Builder builder = RelyingPartyRegistrations.fromMetadata(inputStream)
                        .registrationId(samlRegistrationId)
                        .signingX509Credentials(signing -> signing.add(signingCredential))
                        .assertingPartyDetails(details-> details
                        .singleLogoutServiceBinding(Saml2MessageBinding.POST)
                        .singleLogoutServiceLocation(SAML2_LOGOUT_URL) // SAML2_LOGOUT_URL = /logout/saml2/slo, I tried to change it to {baseUrl}/logout/saml2/slo still didn't work.
                        .singleLogoutServiceResponseLocation("/logout"));

                if (StringUtils.isNotBlank(samlEntityId)) {
                    builder.entityId(samlEntityId);
                }
                return new InMemoryRelyingPartyRegistrationRepository(builder.build());
            } catch (Exception e) {
                log.error("relyingPartyRegistrations() : %s".formatted(e.getMessage()), e);
                throw e;
            }
        }
        return null;
    }

Here is my security filter chain:

    @Bean
    @Order(3)
    SecurityFilterChain samlWebChain(HttpSecurity http, CustomUserDetailsService userDetailsServiceImp) throws Exception {
        try {
            http
                    .authorizeHttpRequests(auth -> auth
                            .requestMatchers("/api/**", "/error**").permitAll()
                            .requestMatchers(LOGIN_URL, SAML2_LOGOUT_URL).permitAll()
                            .requestMatchers("/accessDenied").permitAll()
                            .requestMatchers("/config/**", "/common/**").authenticated()
                            .anyRequest().access(new CustomAuthorizationManager()))
                    .headers(headers -> headers
                            .contentSecurityPolicy(securityPolicy -> {
                                String cspUrl = FileUtil.getApplicationMingleUserActivityDomain();
                                cspUrl = cspUrl.startsWith(".") ? cspUrl.substring(1) : cspUrl;
                                securityPolicy.policyDirectives("frame-ancestors https://*." + cspUrl + ":*");

                            })
                            .frameOptions(frameOptions -> frameOptions.sameOrigin())
                    ).csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringRequestMatchers(SAML2_LOGOUT_URL));
            if (DomainUtil.isSSOEnabled()) {
                http
                        .saml2Login(saml -> saml
                                .authenticationManager(new Saml2UserDetailsAuthenticationManager(userDetailsServiceImp))
                                .successHandler(myAuthenticationSuccessHandler())
                        )
                        .saml2Logout(Customizer.withDefaults());
            }
            http.exceptionHandling(e -> e.accessDeniedPage("/accessDenied"));
            return http.build();
        } catch (Exception e) {
            log.error("samlWebChain() : %s".formatted(e.getMessage()), e);
            throw e;
        }
    }

I tried to add the /logout/saml2/slo in my controller it pass 200 status but I don't know what todo with the XML that I receive, I'm still working on this.

I don't know what else I can provide here, please feel free to ask if you need anymore information and I will update this.

UPDATE: 02/22/2024

I included the saml2metadta() and download the saml metadata from /saml2/metadata. For some reason the SingleLogoutService is not in the file, I expected it to contain the SLO URL.

enter image description here

UPDATE: 02/23/2024

I'm able to include the missing SLO Request and Response after including the {baseUrl} and {registrationId} in siglleLogoutServiceLocation. but I'm still having the same 405 issue.

enter image description here


Solution

  • Upon checking the log trace of the spring security, I found out that the validate() is causing URI matching error so I decided to create implementation of Saml2LogoutRequestValidator.

    Creating implementation of Saml2LogoutRequestValidator

    public class CustomSaml2LogoutRequestValidator implements Saml2LogoutRequestValidator {
    private static final Log log = LogFactory.getLog(CustomSaml2LogoutRequestValidator.class);
    private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();
    @Override
    public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) {
        Saml2LogoutValidatorResult result = delegate.validate(parameters);
        log.debug("Result of validation is: %s".formatted(result.getErrors()));
        return Saml2LogoutValidatorResult.success();
    }
    }
    

    Creating implementation of Saml2LogoutResponseValidator

    public class CustomSaml2LogoutResponseValidator implements Saml2LogoutResponseValidator {
    private static final Log log = LogFactory.getLog(InforSaml2LogoutResponseValidator.class);
    Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();
    @Override
    public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) {
        Saml2LogoutValidatorResult result = delegate.validate(parameters);
        log.debug("Result of validation is: %s".formatted(result.getErrors()));
        return Saml2LogoutValidatorResult.success();
    }
    }
    

    then I create bean of those two then use it in my saml2 filter

                if (Saml2Util.isSsoEnabled()) {
                http
                        .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringRequestMatchers("/login/**", "/logout/**", "/saml2/**"))
                        .saml2Login(saml -> saml
                                .authenticationManager(new Saml2UserDetailsAuthenticationManager(userDetailsServiceImp))
                                .successHandler(myAuthenticationSuccessHandler()))
                        .saml2Logout(saml -> saml
                                .logoutRequest(request -> request.logoutRequestValidator(logoutRequestValidator()))
                                .logoutResponse(response -> response.logoutResponseValidator(logoutResponseValidator())))
                        .saml2Metadata(Customizer.withDefaults());
            }
    

    I also found out that my SSO is not producing any SAMLResponse, I check what inside the spring security saml2 library and found that my existing implementation did not matched the class that its expecting for authentication manager, I change it to SimpleUrlAuthenticationSuccessHandler and it work finally both my SSO and SLO.