Jelajahi Sumber

Send saml logout response even when validation errors happen

Signed-off-by: Liviu Gheorghe <liviu.gheorghe.ro@gmail.com>
Liviu Gheorghe 1 tahun lalu
induk
melakukan
eaf8184142

+ 25 - 1
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -130,6 +130,30 @@ public final class Saml2ErrorCodes {
 	 */
 	public static final String INVALID_IN_RESPONSE_TO = "invalid_in_response_to";
 
+	/**
+	 * The RP registration does not have configured a logout request endpoint
+	 * @since 6.3
+	 */
+	public static final String MISSING_LOGOUT_REQUEST_ENDPOINT = "missing_logout_request_endpoint";
+
+	/**
+	 * The saml response or logout request was delivered via an invalid binding
+	 * @since 6.3
+	 */
+	public static final String INVALID_BINDING = "invalid_binding";
+
+	/**
+	 * The saml logout request failed validation
+	 * @since 6.3
+	 */
+	public static final String INVALID_LOGOUT_REQUEST = "invalid_logout_request";
+
+	/**
+	 * The saml logout response could not be generated
+	 * @since 6.3
+	 */
+	public static final String FAILED_TO_GENERATE_LOGOUT_RESPONSE = "failed_to_generate_logout_response";
+
 	private Saml2ErrorCodes() {
 	}
 

+ 24 - 1
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/BaseOpenSamlLogoutResponseResolver.java

@@ -43,8 +43,11 @@ import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
 
 import org.springframework.security.core.Authentication;
 import org.springframework.security.saml2.core.OpenSamlInitializationService;
+import org.springframework.security.saml2.core.Saml2Error;
+import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ParameterNames;
 import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -130,6 +133,16 @@ final class BaseOpenSamlLogoutResponseResolver implements Saml2LogoutResponseRes
 	 */
 	@Override
 	public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
+		return resolve(request, authentication, StatusCode.SUCCESS);
+	}
+
+	@Override
+	public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
+			Saml2AuthenticationException authenticationException) {
+		return resolve(request, authentication, getSamlStatus(authenticationException));
+	}
+
+	private Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication, String statusCode) {
 		LogoutRequest logoutRequest = this.saml.deserialize(extractSamlRequest(request));
 		String registrationId = getRegistrationId(authentication);
 		RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId);
@@ -152,7 +165,7 @@ final class BaseOpenSamlLogoutResponseResolver implements Saml2LogoutResponseRes
 		issuer.setValue(entityId);
 		logoutResponse.setIssuer(issuer);
 		StatusCode code = this.statusCodeBuilder.buildObject();
-		code.setValue(StatusCode.SUCCESS);
+		code.setValue(statusCode);
 		Status status = this.statusBuilder.buildObject();
 		status.setStatusCode(code);
 		logoutResponse.setStatus(status);
@@ -224,6 +237,16 @@ final class BaseOpenSamlLogoutResponseResolver implements Saml2LogoutResponseRes
 		return this.saml.serialize(logoutResponse).serialize();
 	}
 
+	private String getSamlStatus(Saml2AuthenticationException exception) {
+		Saml2Error saml2Error = exception.getSaml2Error();
+		return switch (saml2Error.getErrorCode()) {
+			case Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT, Saml2ErrorCodes.INVALID_BINDING ->
+				StatusCode.REQUEST_DENIED;
+			case Saml2ErrorCodes.INVALID_LOGOUT_REQUEST -> StatusCode.REQUESTER;
+			default -> StatusCode.RESPONDER;
+		};
+	}
+
 	static final class LogoutResponseParameters {
 
 		private final HttpServletRequest request;

+ 56 - 37
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java

@@ -31,6 +31,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.saml2.core.Saml2Error;
+import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ParameterNames;
 import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
 import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
@@ -112,47 +113,84 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
 			throws ServletException, IOException {
 		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
-		Saml2LogoutRequestValidatorParameters parameters;
 		try {
-			parameters = this.logoutRequestResolver.resolve(request, authentication);
+			Saml2LogoutRequestValidatorParameters parameters = this.logoutRequestResolver.resolve(request,
+					authentication);
+			if (parameters == null) {
+				chain.doFilter(request, response);
+				return;
+			}
+
+			Saml2LogoutResponse logoutResponse = processLogoutRequest(request, response, authentication, parameters);
+			sendLogoutResponse(request, response, logoutResponse);
 		}
 		catch (Saml2AuthenticationException ex) {
-			this.logger.trace("Did not process logout request since failed to find requested RelyingPartyRegistration");
-			response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-			return;
-		}
-		if (parameters == null) {
-			chain.doFilter(request, response);
-			return;
+			Saml2LogoutResponse errorLogoutResponse = this.logoutResponseResolver.resolve(request, authentication, ex);
+			if (errorLogoutResponse == null) {
+				this.logger.trace("Returning error since no error logout response could be generated", ex);
+				response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+				return;
+			}
+
+			sendLogoutResponse(request, response, errorLogoutResponse);
 		}
+	}
+
+	public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
+		Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
+		Assert.isInstanceOf(Saml2AssertingPartyLogoutRequestResolver.class, this.logoutRequestResolver,
+				"saml2LogoutRequestResolver and logoutRequestMatcher cannot both be set. Please set the request matcher in the saml2LogoutRequestResolver itself.");
+		((Saml2AssertingPartyLogoutRequestResolver) this.logoutRequestResolver)
+			.setLogoutRequestMatcher(logoutRequestMatcher);
+	}
+
+	/**
+	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
+	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
+	 *
+	 * @since 5.8
+	 */
+	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
+		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
+		this.securityContextHolderStrategy = securityContextHolderStrategy;
+	}
+
+	private Saml2LogoutResponse processLogoutRequest(HttpServletRequest request, HttpServletResponse response,
+			Authentication authentication, Saml2LogoutRequestValidatorParameters parameters) {
 		RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
 		if (registration.getSingleLogoutServiceLocation() == null) {
 			this.logger.trace(
 					"Did not process logout request since RelyingPartyRegistration has not been configured with a logout request endpoint");
-			response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
-			return;
+			throw new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT,
+					"RelyingPartyRegistration has not been configured with a logout request endpoint"));
 		}
 
 		Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request);
 		if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) {
 			this.logger.trace("Did not process logout request since used incorrect binding");
-			response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
-			return;
+			throw new Saml2AuthenticationException(
+					new Saml2Error(Saml2ErrorCodes.INVALID_BINDING, "Logout request used invalid binding"));
 		}
 
 		Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters);
 		if (result.hasErrors()) {
-			response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString());
 			this.logger.debug(LogMessage.format("Failed to validate LogoutRequest: %s", result.getErrors()));
-			return;
+			throw new Saml2AuthenticationException(
+					new Saml2Error(Saml2ErrorCodes.INVALID_LOGOUT_REQUEST, "Failed to validate the logout request"));
 		}
+
 		this.handler.logout(request, response, authentication);
 		Saml2LogoutResponse logoutResponse = this.logoutResponseResolver.resolve(request, authentication);
 		if (logoutResponse == null) {
-			this.logger.trace("Returning 401 since no logout response generated");
-			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-			return;
+			this.logger.trace("Returning error since no logout response generated");
+			throw new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.FAILED_TO_GENERATE_LOGOUT_RESPONSE,
+					"Could not generated logout response"));
 		}
+		return logoutResponse;
+	}
+
+	private void sendLogoutResponse(HttpServletRequest request, HttpServletResponse response,
+			Saml2LogoutResponse logoutResponse) throws IOException {
 		if (logoutResponse.getBinding() == Saml2MessageBinding.REDIRECT) {
 			doRedirect(request, response, logoutResponse);
 		}
@@ -161,25 +199,6 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
 		}
 	}
 
-	public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
-		Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
-		Assert.isInstanceOf(Saml2AssertingPartyLogoutRequestResolver.class, this.logoutRequestResolver,
-				"saml2LogoutRequestResolver and logoutRequestMatcher cannot both be set. Please set the request matcher in the saml2LogoutRequestResolver itself.");
-		((Saml2AssertingPartyLogoutRequestResolver) this.logoutRequestResolver)
-			.setLogoutRequestMatcher(logoutRequestMatcher);
-	}
-
-	/**
-	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
-	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
-	 *
-	 * @since 5.8
-	 */
-	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
-		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
-		this.securityContextHolderStrategy = securityContextHolderStrategy;
-	}
-
 	private void doRedirect(HttpServletRequest request, HttpServletResponse response,
 			Saml2LogoutResponse logoutResponse) throws IOException {
 		String location = logoutResponse.getResponseLocation();

+ 13 - 1
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.web.authentication.l
 import jakarta.servlet.http.HttpServletRequest;
 
 import org.springframework.security.core.Authentication;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 
@@ -44,4 +45,15 @@ public interface Saml2LogoutResponseResolver {
 	 */
 	Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication);
 
+	/**
+	 * Prepare to create, sign, and serialize a SAML 2.0 Error Logout Response.
+	 * @param request the HTTP request
+	 * @param authentication the current user
+	 * @param authenticationException the thrown exception when the logout request was
+	 * processed
+	 * @return a signed and serialized SAML 2.0 Logout Response
+	 */
+	Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
+			Saml2AuthenticationException authenticationException);
+
 }

+ 10 - 0
saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolver.java

@@ -24,6 +24,7 @@ import jakarta.servlet.http.HttpServletRequest;
 import org.opensaml.saml.saml2.core.LogoutRequest;
 
 import org.springframework.security.core.Authentication;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -66,6 +67,15 @@ public final class OpenSaml4LogoutResponseResolver implements Saml2LogoutRespons
 		return this.delegate.resolve(request, authentication);
 	}
 
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
+			Saml2AuthenticationException exception) {
+		return this.delegate.resolve(request, authentication, exception);
+	}
+
 	/**
 	 * Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
 	 * @param parametersConsumer a consumer that accepts an

+ 50 - 1
saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,17 +17,26 @@
 package org.springframework.security.saml2.provider.service.web.authentication.logout;
 
 import java.util.function.Consumer;
+import java.util.stream.Stream;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.StatusCode;
 
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.saml2.core.Saml2Error;
+import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ParameterNames;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
 import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
 import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
 import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
 import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver.LogoutResponseParameters;
@@ -69,6 +78,27 @@ public class OpenSaml4LogoutResponseResolverTests {
 		verify(parametersConsumer).accept(any());
 	}
 
+	@ParameterizedTest
+	@MethodSource("provideAuthExceptionAndExpectedSamlStatusCode")
+	public void resolveWithAuthException(Saml2AuthenticationException exception, String expectedStatusCode) {
+		OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(
+				this.relyingPartyRegistrationResolver);
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
+			.assertingPartyMetadata(
+					(party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")
+						.singleLogoutServiceBinding(Saml2MessageBinding.POST))
+			.build();
+		Authentication authentication = new TestingAuthenticationToken("user", "password");
+		LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
+		request.setParameter(Saml2ParameterNames.SAML_REQUEST,
+				Saml2Utils.samlEncode(this.saml.serialize(logoutRequest).serialize().getBytes()));
+		given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
+		Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication, exception);
+		assertThat(logoutResponse).isNotNull();
+		assertThat(new String(Saml2Utils.samlDecode(logoutResponse.getSamlResponse()))).contains(expectedStatusCode);
+	}
+
 	@Test
 	public void setParametersConsumerWhenNullThenIllegalArgument() {
 		OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
@@ -77,4 +107,23 @@ public class OpenSaml4LogoutResponseResolverTests {
 			.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
 	}
 
+	private static Stream<Arguments> provideAuthExceptionAndExpectedSamlStatusCode() {
+		return Stream.of(
+				Arguments.of(
+						new Saml2AuthenticationException(
+								new Saml2Error(Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT, "")),
+						StatusCode.REQUEST_DENIED),
+				Arguments.of(new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.INVALID_BINDING, "")),
+						StatusCode.REQUEST_DENIED),
+				Arguments.of(
+						new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.INVALID_LOGOUT_REQUEST, "")),
+						StatusCode.REQUESTER),
+				Arguments.of(
+						new Saml2AuthenticationException(
+								new Saml2Error(Saml2ErrorCodes.FAILED_TO_GENERATE_LOGOUT_RESPONSE, "")),
+						StatusCode.RESPONDER)
+
+		);
+	}
+
 }

+ 10 - 0
saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml5LogoutResponseResolver.java

@@ -24,6 +24,7 @@ import jakarta.servlet.http.HttpServletRequest;
 import org.opensaml.saml.saml2.core.LogoutRequest;
 
 import org.springframework.security.core.Authentication;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -66,6 +67,15 @@ public final class OpenSaml5LogoutResponseResolver implements Saml2LogoutRespons
 		return this.delegate.resolve(request, authentication);
 	}
 
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
+			Saml2AuthenticationException exception) {
+		return this.delegate.resolve(request, authentication, exception);
+	}
+
 	/**
 	 * Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
 	 * @param parametersConsumer a consumer that accepts an

+ 106 - 11
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.core.context.SecurityContextImpl;
 import org.springframework.security.saml2.core.Saml2Error;
+import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ParameterNames;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
 import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
@@ -40,6 +41,7 @@ import org.springframework.security.web.authentication.logout.LogoutHandler;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.BDDMockito.mock;
 import static org.mockito.Mockito.verify;
@@ -117,12 +119,7 @@ public class Saml2LogoutRequestFilterTests {
 		verify(this.logoutRequestValidator).validate(any());
 		verify(this.logoutHandler).logout(any(), any(), any());
 		verify(this.logoutResponseResolver).resolve(any(), any());
-		String content = response.getContentAsString();
-		assertThat(content).contains(Saml2ParameterNames.SAML_RESPONSE);
-		assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
-		assertThat(content).contains(
-				"<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">");
-		assertThat(content).contains("<script>window.onload = function() { document.forms[0].submit(); }</script>");
+		checkResponse(response.getContentAsString(), registration);
 		verify(this.securityContextHolderStrategy).getContext();
 	}
 
@@ -150,24 +147,38 @@ public class Saml2LogoutRequestFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenValidationFailsThen401() throws Exception {
-		RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
+	public void doFilterWhenValidationFailsErrorLogoutResponseIsPosted() throws Exception {
+		RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
+			.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST))
+			.build();
 		Authentication authentication = new TestingAuthenticationToken("user", "password");
 		SecurityContextHolder.getContext().setAuthentication(authentication);
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
 		request.setServletPath("/logout/saml2/slo");
 		request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
 		MockHttpServletResponse response = new MockHttpServletResponse();
+		Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
+			.samlResponse("response")
+			.build();
+
 		given(this.relyingPartyRegistrationResolver.resolve(request, null)).willReturn(registration);
 		given(this.logoutRequestValidator.validate(any()))
 			.willReturn(Saml2LogoutValidatorResult.withErrors(new Saml2Error("error", "description")).build());
+		given(this.logoutResponseResolver.resolve(any(), any(),
+				argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_LOGOUT_REQUEST))))
+			.willReturn(logoutResponse);
+
 		this.logoutRequestProcessingFilter.doFilter(request, response, new MockFilterChain());
-		assertThat(response.getStatus()).isEqualTo(401);
+
+		checkResponse(response.getContentAsString(), registration);
+		verify(this.logoutRequestValidator).validate(any());
+		verify(this.logoutResponseResolver).resolve(any(), any(),
+				argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_LOGOUT_REQUEST)));
 		verifyNoInteractions(this.logoutHandler);
 	}
 
 	@Test
-	public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception {
+	public void doFilterWhenNoRelyingErrorLogoutResponseIsPosted() throws Exception {
 		Authentication authentication = new TestingAuthenticationToken("user", "password");
 		SecurityContextHolder.getContext().setAuthentication(authentication);
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
@@ -175,12 +186,96 @@ public class Saml2LogoutRequestFilterTests {
 		request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
+			.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST))
 			.singleLogoutServiceLocation(null)
 			.build();
+		Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
+			.samlResponse("response")
+			.build();
+
+		given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
+		given(this.logoutResponseResolver.resolve(any(), any(), argThat(
+				(ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT))))
+			.willReturn(logoutResponse);
+
+		this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
+
+		checkResponse(response.getContentAsString(), registration);
+		verify(this.logoutResponseResolver).resolve(any(), any(), argThat(
+				(ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT)));
+		verifyNoInteractions(this.logoutHandler);
+	}
+
+	@Test
+	public void doFilterWhenInvalidBindingErrorLogoutResponseIsPosted() throws Exception {
+		Authentication authentication = new TestingAuthenticationToken("user", "password");
+		SecurityContextHolder.getContext().setAuthentication(authentication);
+		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
+		request.setServletPath("/logout/saml2/slo");
+		request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
+			.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST))
+			.singleLogoutServiceBindings((bindings) -> {
+				bindings.clear();
+				bindings.add(Saml2MessageBinding.REDIRECT);
+			})
+			.build();
+		Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
+			.samlResponse("response")
+			.build();
+
+		given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
+		given(this.logoutResponseResolver.resolve(any(), any(),
+				argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_BINDING))))
+			.willReturn(logoutResponse);
+
+		this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
+
+		checkResponse(response.getContentAsString(), registration);
+		verify(this.logoutResponseResolver).resolve(any(), any(),
+				argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_BINDING)));
+		verifyNoInteractions(this.logoutHandler);
+	}
+
+	@Test
+	public void doFilterWhenNoErrorResponseCanBeGeneratedThen401() throws Exception {
+		Authentication authentication = new TestingAuthenticationToken("user", "password");
+		SecurityContextHolder.getContext().setAuthentication(authentication);
+		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
+		request.setServletPath("/logout/saml2/slo");
+		request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request");
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
+			.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST))
+			.singleLogoutServiceBindings((bindings) -> {
+				bindings.clear();
+				bindings.add(Saml2MessageBinding.REDIRECT);
+			})
+			.build();
+
 		given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
+		given(this.logoutResponseResolver.resolve(any(), any(),
+				argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_BINDING))))
+			.willReturn(null);
+
 		this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
+
 		assertThat(response.getStatus()).isEqualTo(401);
+		verify(this.logoutResponseResolver).resolve(any(), any(),
+				argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_BINDING)));
 		verifyNoInteractions(this.logoutHandler);
 	}
 
+	private void checkResponse(String responseContent, RelyingPartyRegistration registration) {
+		assertThat(responseContent).contains(Saml2ParameterNames.SAML_RESPONSE);
+		assertThat(responseContent)
+			.contains(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
+		assertThat(responseContent).contains(
+				"<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">");
+		assertThat(responseContent)
+			.contains("<script>window.onload = function() { document.forms[0].submit(); }</script>");
+
+	}
+
 }