What is the correct way to use Single Logout URL? 403 Forbidden + Code

I’m currently implementing a custom service provider initiated single sign on (SP-initiated SSO) and single logout (SLO) using SAML 2.0. The implementation uses Spring Security 5.8 in Java 17. On Okta, I have already enabled Single Logout.

image

However, I’m still having some troubles with Single Logout, as I keep getting 403 Forbidden errors. I’m currently going through the official Spring docs on performing single logout here.

The expected SLO workflow is given as such:

  1. Logout the user and invalidate the session
  2. Use a Saml2LogoutRequestResolver to create, sign, and serialize a <saml2:LogoutRequest> based on the RelyingPartyRegistration associated with the currently logged-in user.
  3. Send a redirect or post to the asserting party based on the RelyingPartyRegistration
  4. Deserialize, verify, and process the <saml2:LogoutResponse> sent by the asserting party
  5. Redirect to any configured successful logout endpoint.

Right now, I’m stuck on Step 3. I have an endpoint on a Spring MVC controller that sends a POST request to the Single Logout URL, but I am confused as to whether or not to include the successful Saml2LogoutRequest payload that has already been resolved to Okta. There are no known information about what to do with the SLO URL.

@RequestMapping ( value = "/saml2Logout", method = RequestMethod.POST )
public ResponseEntity<String> ssoLogout ( HttpServletRequest request, Authentication auth, RedirectAttributes redirectAttributes ) {
	Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) auth.getPrincipal();
	SsoMetadata data = this.ssoProperties.getSso().get(principal.getRelyingPartyRegistrationId());
	String logoutUrl = data.getSsoSingleLogoutUrl();
	String registrationId = principal.getRelyingPartyRegistrationId();
	try {
		ClassPathResource privateKeyResource = new ClassPathResource(String.format("saml/%s/%s_signout.key", registrationId, registrationId));
		RSAPrivateKey privateKey = RsaKeyConverters.pkcs8().convert(privateKeyResource.getInputStream());
		ClassPathResource certificateResource = new ClassPathResource(String.format("saml/%s/%s_signout.crt", registrationId, registrationId));
		X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificateResource.getInputStream());
		RelyingPartyRegistration newReg = RelyingPartyRegistration
			.withRelyingPartyRegistration(relyingPartyRegistrationRepository.findByRegistrationId(principal.getRelyingPartyRegistrationId()))
			.assertingPartyDetails(details -> details.singleLogoutServiceLocation(logoutUrl))
			.signingX509Credentials(creds -> creds.add(Saml2X509Credential.signing(privateKey, certificate)))
			.build();
		RelyingPartyRegistrationRepository repo = new InMemoryRelyingPartyRegistrationRepository(Collections.singletonList(newReg));
		RelyingPartyRegistrationResolver resolver = new DefaultRelyingPartyRegistrationResolver(repo);
		Saml2LogoutRequestResolver saml2Resolver = new OpenSaml4LogoutRequestResolver(resolver);
		Saml2LogoutRequest logoutRequest = saml2Resolver.resolve(request, auth);

		WebClient.Builder builder = WebClient.builder();
		WebClient client = builder
			.baseUrl(logoutUrl)
			.clientConnector(new ReactorClientHttpConnector(HttpClient.create().followRedirect(true)))
			.build();
		ResponseEntity<String> response = client
			.post()
			.body(BodyInserters.fromValue(logoutRequest.getSamlRequest()))
			.retrieve()
			.toEntity(String.class)
			.block();
		// What do you really do with this response anyway???
		return response;
	} catch (IOException | CertificateException e) {
		logger.error("Unable to create signing credentials.");
	}
	return null;
}

My question is, what do you do with Single Logout URL in the service provider, particularly for custom implementations using Spring Security?