Skip to content

Commit eaf8184

Browse files
1livvjzheaux
authored andcommitted
Send saml logout response even when validation errors happen
Signed-off-by: Liviu Gheorghe <[email protected]>
1 parent 097640b commit eaf8184

File tree

8 files changed

+294
-52
lines changed

8 files changed

+294
-52
lines changed

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -130,6 +130,30 @@ public final class Saml2ErrorCodes {
130130
*/
131131
public static final String INVALID_IN_RESPONSE_TO = "invalid_in_response_to";
132132

133+
/**
134+
* The RP registration does not have configured a logout request endpoint
135+
* @since 6.3
136+
*/
137+
public static final String MISSING_LOGOUT_REQUEST_ENDPOINT = "missing_logout_request_endpoint";
138+
139+
/**
140+
* The saml response or logout request was delivered via an invalid binding
141+
* @since 6.3
142+
*/
143+
public static final String INVALID_BINDING = "invalid_binding";
144+
145+
/**
146+
* The saml logout request failed validation
147+
* @since 6.3
148+
*/
149+
public static final String INVALID_LOGOUT_REQUEST = "invalid_logout_request";
150+
151+
/**
152+
* The saml logout response could not be generated
153+
* @since 6.3
154+
*/
155+
public static final String FAILED_TO_GENERATE_LOGOUT_RESPONSE = "failed_to_generate_logout_response";
156+
133157
private Saml2ErrorCodes() {
134158
}
135159

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@
4343

4444
import org.springframework.security.core.Authentication;
4545
import org.springframework.security.saml2.core.OpenSamlInitializationService;
46+
import org.springframework.security.saml2.core.Saml2Error;
47+
import org.springframework.security.saml2.core.Saml2ErrorCodes;
4648
import org.springframework.security.saml2.core.Saml2ParameterNames;
4749
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
50+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
4851
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
4952
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
5053
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -130,6 +133,16 @@ final class BaseOpenSamlLogoutResponseResolver implements Saml2LogoutResponseRes
130133
*/
131134
@Override
132135
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
136+
return resolve(request, authentication, StatusCode.SUCCESS);
137+
}
138+
139+
@Override
140+
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
141+
Saml2AuthenticationException authenticationException) {
142+
return resolve(request, authentication, getSamlStatus(authenticationException));
143+
}
144+
145+
private Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication, String statusCode) {
133146
LogoutRequest logoutRequest = this.saml.deserialize(extractSamlRequest(request));
134147
String registrationId = getRegistrationId(authentication);
135148
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId);
@@ -152,7 +165,7 @@ public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication au
152165
issuer.setValue(entityId);
153166
logoutResponse.setIssuer(issuer);
154167
StatusCode code = this.statusCodeBuilder.buildObject();
155-
code.setValue(StatusCode.SUCCESS);
168+
code.setValue(statusCode);
156169
Status status = this.statusBuilder.buildObject();
157170
status.setStatusCode(code);
158171
logoutResponse.setStatus(status);
@@ -224,6 +237,16 @@ private String serialize(LogoutResponse logoutResponse) {
224237
return this.saml.serialize(logoutResponse).serialize();
225238
}
226239

240+
private String getSamlStatus(Saml2AuthenticationException exception) {
241+
Saml2Error saml2Error = exception.getSaml2Error();
242+
return switch (saml2Error.getErrorCode()) {
243+
case Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT, Saml2ErrorCodes.INVALID_BINDING ->
244+
StatusCode.REQUEST_DENIED;
245+
case Saml2ErrorCodes.INVALID_LOGOUT_REQUEST -> StatusCode.REQUESTER;
246+
default -> StatusCode.RESPONDER;
247+
};
248+
}
249+
227250
static final class LogoutResponseParameters {
228251

229252
private final HttpServletRequest request;

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

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.security.core.context.SecurityContextHolder;
3232
import org.springframework.security.core.context.SecurityContextHolderStrategy;
3333
import org.springframework.security.saml2.core.Saml2Error;
34+
import org.springframework.security.saml2.core.Saml2ErrorCodes;
3435
import org.springframework.security.saml2.core.Saml2ParameterNames;
3536
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
3637
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
@@ -112,47 +113,84 @@ public Saml2LogoutRequestFilter(RelyingPartyRegistrationResolver relyingPartyReg
112113
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
113114
throws ServletException, IOException {
114115
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
115-
Saml2LogoutRequestValidatorParameters parameters;
116116
try {
117-
parameters = this.logoutRequestResolver.resolve(request, authentication);
117+
Saml2LogoutRequestValidatorParameters parameters = this.logoutRequestResolver.resolve(request,
118+
authentication);
119+
if (parameters == null) {
120+
chain.doFilter(request, response);
121+
return;
122+
}
123+
124+
Saml2LogoutResponse logoutResponse = processLogoutRequest(request, response, authentication, parameters);
125+
sendLogoutResponse(request, response, logoutResponse);
118126
}
119127
catch (Saml2AuthenticationException ex) {
120-
this.logger.trace("Did not process logout request since failed to find requested RelyingPartyRegistration");
121-
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
122-
return;
123-
}
124-
if (parameters == null) {
125-
chain.doFilter(request, response);
126-
return;
128+
Saml2LogoutResponse errorLogoutResponse = this.logoutResponseResolver.resolve(request, authentication, ex);
129+
if (errorLogoutResponse == null) {
130+
this.logger.trace("Returning error since no error logout response could be generated", ex);
131+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
132+
return;
133+
}
134+
135+
sendLogoutResponse(request, response, errorLogoutResponse);
127136
}
137+
}
138+
139+
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
140+
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
141+
Assert.isInstanceOf(Saml2AssertingPartyLogoutRequestResolver.class, this.logoutRequestResolver,
142+
"saml2LogoutRequestResolver and logoutRequestMatcher cannot both be set. Please set the request matcher in the saml2LogoutRequestResolver itself.");
143+
((Saml2AssertingPartyLogoutRequestResolver) this.logoutRequestResolver)
144+
.setLogoutRequestMatcher(logoutRequestMatcher);
145+
}
146+
147+
/**
148+
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
149+
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
150+
*
151+
* @since 5.8
152+
*/
153+
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
154+
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
155+
this.securityContextHolderStrategy = securityContextHolderStrategy;
156+
}
157+
158+
private Saml2LogoutResponse processLogoutRequest(HttpServletRequest request, HttpServletResponse response,
159+
Authentication authentication, Saml2LogoutRequestValidatorParameters parameters) {
128160
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
129161
if (registration.getSingleLogoutServiceLocation() == null) {
130162
this.logger.trace(
131163
"Did not process logout request since RelyingPartyRegistration has not been configured with a logout request endpoint");
132-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
133-
return;
164+
throw new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT,
165+
"RelyingPartyRegistration has not been configured with a logout request endpoint"));
134166
}
135167

136168
Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request);
137169
if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) {
138170
this.logger.trace("Did not process logout request since used incorrect binding");
139-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
140-
return;
171+
throw new Saml2AuthenticationException(
172+
new Saml2Error(Saml2ErrorCodes.INVALID_BINDING, "Logout request used invalid binding"));
141173
}
142174

143175
Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters);
144176
if (result.hasErrors()) {
145-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString());
146177
this.logger.debug(LogMessage.format("Failed to validate LogoutRequest: %s", result.getErrors()));
147-
return;
178+
throw new Saml2AuthenticationException(
179+
new Saml2Error(Saml2ErrorCodes.INVALID_LOGOUT_REQUEST, "Failed to validate the logout request"));
148180
}
181+
149182
this.handler.logout(request, response, authentication);
150183
Saml2LogoutResponse logoutResponse = this.logoutResponseResolver.resolve(request, authentication);
151184
if (logoutResponse == null) {
152-
this.logger.trace("Returning 401 since no logout response generated");
153-
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
154-
return;
185+
this.logger.trace("Returning error since no logout response generated");
186+
throw new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.FAILED_TO_GENERATE_LOGOUT_RESPONSE,
187+
"Could not generated logout response"));
155188
}
189+
return logoutResponse;
190+
}
191+
192+
private void sendLogoutResponse(HttpServletRequest request, HttpServletResponse response,
193+
Saml2LogoutResponse logoutResponse) throws IOException {
156194
if (logoutResponse.getBinding() == Saml2MessageBinding.REDIRECT) {
157195
doRedirect(request, response, logoutResponse);
158196
}
@@ -161,25 +199,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
161199
}
162200
}
163201

164-
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
165-
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
166-
Assert.isInstanceOf(Saml2AssertingPartyLogoutRequestResolver.class, this.logoutRequestResolver,
167-
"saml2LogoutRequestResolver and logoutRequestMatcher cannot both be set. Please set the request matcher in the saml2LogoutRequestResolver itself.");
168-
((Saml2AssertingPartyLogoutRequestResolver) this.logoutRequestResolver)
169-
.setLogoutRequestMatcher(logoutRequestMatcher);
170-
}
171-
172-
/**
173-
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
174-
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
175-
*
176-
* @since 5.8
177-
*/
178-
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
179-
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
180-
this.securityContextHolderStrategy = securityContextHolderStrategy;
181-
}
182-
183202
private void doRedirect(HttpServletRequest request, HttpServletResponse response,
184203
Saml2LogoutResponse logoutResponse) throws IOException {
185204
String location = logoutResponse.getResponseLocation();

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import jakarta.servlet.http.HttpServletRequest;
2020

2121
import org.springframework.security.core.Authentication;
22+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
2223
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
2324
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
2425

@@ -44,4 +45,15 @@ public interface Saml2LogoutResponseResolver {
4445
*/
4546
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication);
4647

48+
/**
49+
* Prepare to create, sign, and serialize a SAML 2.0 Error Logout Response.
50+
* @param request the HTTP request
51+
* @param authentication the current user
52+
* @param authenticationException the thrown exception when the logout request was
53+
* processed
54+
* @return a signed and serialized SAML 2.0 Logout Response
55+
*/
56+
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
57+
Saml2AuthenticationException authenticationException);
58+
4759
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.opensaml.saml.saml2.core.LogoutRequest;
2525

2626
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
2728
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
2829
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
2930
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -66,6 +67,15 @@ public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication au
6667
return this.delegate.resolve(request, authentication);
6768
}
6869

70+
/**
71+
* {@inheritDoc}
72+
*/
73+
@Override
74+
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
75+
Saml2AuthenticationException exception) {
76+
return this.delegate.resolve(request, authentication, exception);
77+
}
78+
6979
/**
7080
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
7181
* @param parametersConsumer a consumer that accepts an

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,17 +17,26 @@
1717
package org.springframework.security.saml2.provider.service.web.authentication.logout;
1818

1919
import java.util.function.Consumer;
20+
import java.util.stream.Stream;
2021

2122
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
2226
import org.opensaml.saml.saml2.core.LogoutRequest;
27+
import org.opensaml.saml.saml2.core.StatusCode;
2328

2429
import org.springframework.mock.web.MockHttpServletRequest;
2530
import org.springframework.security.authentication.TestingAuthenticationToken;
2631
import org.springframework.security.core.Authentication;
32+
import org.springframework.security.saml2.core.Saml2Error;
33+
import org.springframework.security.saml2.core.Saml2ErrorCodes;
2734
import org.springframework.security.saml2.core.Saml2ParameterNames;
35+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
2836
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
2937
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
3038
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
39+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
3140
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
3241
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
3342
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver.LogoutResponseParameters;
@@ -69,6 +78,27 @@ public void resolveWhenCustomParametersConsumerThenUses() {
6978
verify(parametersConsumer).accept(any());
7079
}
7180

81+
@ParameterizedTest
82+
@MethodSource("provideAuthExceptionAndExpectedSamlStatusCode")
83+
public void resolveWithAuthException(Saml2AuthenticationException exception, String expectedStatusCode) {
84+
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(
85+
this.relyingPartyRegistrationResolver);
86+
MockHttpServletRequest request = new MockHttpServletRequest();
87+
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration()
88+
.assertingPartyMetadata(
89+
(party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")
90+
.singleLogoutServiceBinding(Saml2MessageBinding.POST))
91+
.build();
92+
Authentication authentication = new TestingAuthenticationToken("user", "password");
93+
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
94+
request.setParameter(Saml2ParameterNames.SAML_REQUEST,
95+
Saml2Utils.samlEncode(this.saml.serialize(logoutRequest).serialize().getBytes()));
96+
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
97+
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication, exception);
98+
assertThat(logoutResponse).isNotNull();
99+
assertThat(new String(Saml2Utils.samlDecode(logoutResponse.getSamlResponse()))).contains(expectedStatusCode);
100+
}
101+
72102
@Test
73103
public void setParametersConsumerWhenNullThenIllegalArgument() {
74104
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
@@ -77,4 +107,23 @@ public void setParametersConsumerWhenNullThenIllegalArgument() {
77107
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
78108
}
79109

110+
private static Stream<Arguments> provideAuthExceptionAndExpectedSamlStatusCode() {
111+
return Stream.of(
112+
Arguments.of(
113+
new Saml2AuthenticationException(
114+
new Saml2Error(Saml2ErrorCodes.MISSING_LOGOUT_REQUEST_ENDPOINT, "")),
115+
StatusCode.REQUEST_DENIED),
116+
Arguments.of(new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.INVALID_BINDING, "")),
117+
StatusCode.REQUEST_DENIED),
118+
Arguments.of(
119+
new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.INVALID_LOGOUT_REQUEST, "")),
120+
StatusCode.REQUESTER),
121+
Arguments.of(
122+
new Saml2AuthenticationException(
123+
new Saml2Error(Saml2ErrorCodes.FAILED_TO_GENERATE_LOGOUT_RESPONSE, "")),
124+
StatusCode.RESPONDER)
125+
126+
);
127+
}
128+
80129
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.opensaml.saml.saml2.core.LogoutRequest;
2525

2626
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
2728
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
2829
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
2930
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -66,6 +67,15 @@ public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication au
6667
return this.delegate.resolve(request, authentication);
6768
}
6869

70+
/**
71+
* {@inheritDoc}
72+
*/
73+
@Override
74+
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
75+
Saml2AuthenticationException exception) {
76+
return this.delegate.resolve(request, authentication, exception);
77+
}
78+
6979
/**
7080
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
7181
* @param parametersConsumer a consumer that accepts an

0 commit comments

Comments
 (0)