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.
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.
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.
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.