Skip to content

Commit 1a6aeca

Browse files
committed
Support multiple SingleLogoutService bindings.
Closes gh-11286
1 parent bcd1047 commit 1a6aeca

File tree

7 files changed

+138
-46
lines changed

7 files changed

+138
-46
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java

Lines changed: 4 additions & 4 deletions
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-2022 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.
@@ -147,7 +147,7 @@ public Saml2LogoutConfigurer(ApplicationContext context) {
147147
* <p>
148148
* The Relying Party triggers logout by POSTing to the endpoint. The Asserting Party
149149
* triggers logout based on what is specified by
150-
* {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
150+
* {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
151151
* @param logoutUrl the URL that will invoke logout
152152
* @return the {@link LogoutConfigurer} for further customizations
153153
* @see LogoutConfigurer#logoutUrl(String)
@@ -343,7 +343,7 @@ public final class LogoutRequestConfigurer {
343343
*
344344
* <p>
345345
* The Asserting Party should use whatever HTTP method specified in
346-
* {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
346+
* {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
347347
* @param logoutUrl the URL that will receive the SAML 2.0 Logout Request
348348
* @return the {@link LogoutRequestConfigurer} for further customizations
349349
* @see Saml2LogoutConfigurer#logoutUrl(String)
@@ -425,7 +425,7 @@ public final class LogoutResponseConfigurer {
425425
*
426426
* <p>
427427
* The Asserting Party should use whatever HTTP method specified in
428-
* {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
428+
* {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
429429
* @param logoutUrl the URL that will receive the SAML 2.0 Logout Response
430430
* @return the {@link LogoutResponseConfigurer} for further customizations
431431
* @see Saml2LogoutConfigurer#logoutUrl(String)

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.security.saml2.core.OpenSamlInitializationService;
4747
import org.springframework.security.saml2.core.Saml2X509Credential;
4848
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
49+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
4950
import org.springframework.util.Assert;
5051

5152
/**
@@ -104,7 +105,9 @@ private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registrati
104105
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
105106
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
106107
if (registration.getSingleLogoutServiceLocation() != null) {
107-
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
108+
for (Saml2MessageBinding binding : registration.getSingleLogoutServiceBindings()) {
109+
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration, binding));
110+
}
108111
}
109112
if (registration.getNameIdFormat() != null) {
110113
spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration));
@@ -147,11 +150,12 @@ private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegis
147150
return assertionConsumerService;
148151
}
149152

150-
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration) {
153+
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration,
154+
Saml2MessageBinding binding) {
151155
SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
152156
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
153157
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
154-
singleLogoutService.setBinding(registration.getSingleLogoutServiceBinding().getUrn());
158+
singleLogoutService.setBinding(binding.getUrn());
155159
return singleLogoutService;
156160
}
157161

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.security.saml2.core.Saml2X509Credential;
3030
import org.springframework.util.Assert;
31+
import org.springframework.util.CollectionUtils;
3132

3233
/**
3334
* Represents a configured relying party (aka Service Provider) and asserting party (aka
@@ -81,7 +82,7 @@ public final class RelyingPartyRegistration {
8182

8283
private final String singleLogoutServiceResponseLocation;
8384

84-
private final Saml2MessageBinding singleLogoutServiceBinding;
85+
private final Collection<Saml2MessageBinding> singleLogoutServiceBindings;
8586

8687
private final String nameIdFormat;
8788

@@ -93,16 +94,16 @@ public final class RelyingPartyRegistration {
9394

9495
private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
9596
Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation,
96-
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding,
97+
String singleLogoutServiceResponseLocation, Collection<Saml2MessageBinding> singleLogoutServiceBindings,
9798
AssertingPartyDetails assertingPartyDetails, String nameIdFormat,
9899
Collection<Saml2X509Credential> decryptionX509Credentials,
99100
Collection<Saml2X509Credential> signingX509Credentials) {
100101
Assert.hasText(registrationId, "registrationId cannot be empty");
101102
Assert.hasText(entityId, "entityId cannot be empty");
102103
Assert.hasText(assertionConsumerServiceLocation, "assertionConsumerServiceLocation cannot be empty");
103104
Assert.notNull(assertionConsumerServiceBinding, "assertionConsumerServiceBinding cannot be null");
104-
Assert.isTrue(singleLogoutServiceLocation == null || singleLogoutServiceBinding != null,
105-
"singleLogoutServiceBinding cannot be null when singleLogoutServiceLocation is set");
105+
Assert.isTrue(singleLogoutServiceLocation == null || !CollectionUtils.isEmpty(singleLogoutServiceBindings),
106+
"singleLogoutServiceBindings cannot be null or empty when singleLogoutServiceLocation is set");
106107
Assert.notNull(assertingPartyDetails, "assertingPartyDetails cannot be null");
107108
Assert.notNull(decryptionX509Credentials, "decryptionX509Credentials cannot be null");
108109
for (Saml2X509Credential c : decryptionX509Credentials) {
@@ -121,7 +122,7 @@ private RelyingPartyRegistration(String registrationId, String entityId, String
121122
this.assertionConsumerServiceBinding = assertionConsumerServiceBinding;
122123
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
123124
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
124-
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
125+
this.singleLogoutServiceBindings = Collections.unmodifiableList(new LinkedList<>(singleLogoutServiceBindings));
125126
this.nameIdFormat = nameIdFormat;
126127
this.assertingPartyDetails = assertingPartyDetails;
127128
this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials));
@@ -194,7 +195,22 @@ public Saml2MessageBinding getAssertionConsumerServiceBinding() {
194195
* @since 5.6
195196
*/
196197
public Saml2MessageBinding getSingleLogoutServiceBinding() {
197-
return this.singleLogoutServiceBinding;
198+
Assert.state(this.singleLogoutServiceBindings.size() == 1, "Method does not support multiple bindings.");
199+
return this.singleLogoutServiceBindings.iterator().next();
200+
}
201+
202+
/**
203+
* Get the <a href=
204+
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
205+
* Binding</a>
206+
* <p>
207+
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in the
208+
* relying party's &lt;SPSSODescriptor&gt;.
209+
* @return the SingleLogoutService Binding
210+
* @since 5.8
211+
*/
212+
public Collection<Saml2MessageBinding> getSingleLogoutServiceBindings() {
213+
return this.singleLogoutServiceBindings;
198214
}
199215

200216
/**
@@ -308,7 +324,7 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi
308324
.assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding())
309325
.singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation())
310326
.singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation())
311-
.singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding())
327+
.singleLogoutServiceBindings((c) -> c.addAll(registration.getSingleLogoutServiceBindings()))
312328
.nameIdFormat(registration.getNameIdFormat())
313329
.assertingPartyDetails((assertingParty) -> assertingParty
314330
.entityId(registration.getAssertingPartyDetails().getEntityId())
@@ -737,7 +753,7 @@ public static final class Builder {
737753

738754
private String singleLogoutServiceResponseLocation;
739755

740-
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST;
756+
private Collection<Saml2MessageBinding> singleLogoutServiceBindings = new LinkedHashSet<>();
741757

742758
private String nameIdFormat = null;
743759

@@ -855,7 +871,28 @@ public Builder assertionConsumerServiceBinding(Saml2MessageBinding assertionCons
855871
* @since 5.6
856872
*/
857873
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
858-
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
874+
return this.singleLogoutServiceBindings((saml2MessageBindings) -> {
875+
saml2MessageBindings.clear();
876+
saml2MessageBindings.add(singleLogoutServiceBinding);
877+
});
878+
}
879+
880+
/**
881+
* Apply this {@link Consumer} to the {@link Collection} of
882+
* {@link Saml2MessageBinding}s for the purposes of modifying the <a href=
883+
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
884+
* Binding</a> {@link Collection}.
885+
*
886+
* <p>
887+
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in
888+
* the relying party's &lt;SPSSODescriptor&gt;.
889+
* @param bindingsConsumer - the {@link Consumer} for modifying the
890+
* {@link Collection}
891+
* @return the {@link Builder} for further configuration
892+
* @since 5.8
893+
*/
894+
public Builder singleLogoutServiceBindings(Consumer<Collection<Saml2MessageBinding>> bindingsConsumer) {
895+
bindingsConsumer.accept(this.singleLogoutServiceBindings);
859896
return this;
860897
}
861898

@@ -925,10 +962,15 @@ public RelyingPartyRegistration build() {
925962
if (this.singleLogoutServiceResponseLocation == null) {
926963
this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation;
927964
}
965+
966+
if (this.singleLogoutServiceBindings.isEmpty()) {
967+
this.singleLogoutServiceBindings.add(Saml2MessageBinding.POST);
968+
}
969+
928970
return new RelyingPartyRegistration(this.registrationId, this.entityId,
929971
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
930972
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
931-
this.singleLogoutServiceBinding, this.assertingPartyDetailsBuilder.build(), this.nameIdFormat,
973+
this.singleLogoutServiceBindings, this.assertingPartyDetailsBuilder.build(), this.nameIdFormat,
932974
this.decryptionX509Credentials, this.signingX509Credentials);
933975
}
934976

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

Lines changed: 6 additions & 6 deletions
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-2022 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.
@@ -134,9 +134,7 @@ Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentic
134134
if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) {
135135
return null;
136136
}
137-
String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST);
138-
byte[] b = Saml2Utils.samlDecode(serialized);
139-
LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b));
137+
LogoutRequest logoutRequest = parse(extractSamlRequest(request));
140138
LogoutResponse logoutResponse = this.logoutResponseBuilder.buildObject();
141139
logoutResponse.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
142140
Issuer issuer = this.issuerBuilder.buildObject();
@@ -189,8 +187,10 @@ private String getRegistrationId(Authentication authentication) {
189187
return null;
190188
}
191189

192-
private String inflateIfRequired(RelyingPartyRegistration registration, byte[] b) {
193-
if (registration.getSingleLogoutServiceBinding() == Saml2MessageBinding.REDIRECT) {
190+
private String extractSamlRequest(HttpServletRequest request) {
191+
String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST);
192+
byte[] b = Saml2Utils.samlDecode(serialized);
193+
if (Saml2MessageBindingUtils.isHttpRedirectBinding(request)) {
194194
return Saml2Utils.samlInflate(b);
195195
}
196196
return new String(b, StandardCharsets.UTF_8);

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
122122
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
123123
return;
124124
}
125-
if (!isCorrectBinding(request, registration)) {
125+
126+
Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request);
127+
if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) {
126128
this.logger.trace("Did not process logout request since used incorrect binding");
127129
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
128130
return;
@@ -131,8 +133,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
131133
String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST);
132134
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
133135
.samlRequest(serialized).relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE))
134-
.binding(registration.getSingleLogoutServiceBinding())
135-
.location(registration.getSingleLogoutServiceLocation())
136+
.binding(saml2MessageBinding).location(registration.getSingleLogoutServiceLocation())
136137
.parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG,
137138
request.getParameter(Saml2ParameterNames.SIG_ALG)))
138139
.parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE,
@@ -177,14 +178,6 @@ private String getRegistrationId(Authentication authentication) {
177178
return null;
178179
}
179180

180-
private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) {
181-
Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding();
182-
if (requiredBinding == Saml2MessageBinding.POST) {
183-
return "POST".equals(request.getMethod());
184-
}
185-
return "GET".equals(request.getMethod());
186-
}
187-
188181
private void doRedirect(HttpServletRequest request, HttpServletResponse response,
189182
Saml2LogoutResponse logoutResponse) throws IOException {
190183
String location = logoutResponse.getResponseLocation();

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,18 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
125125
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
126126
return;
127127
}
128-
if (!isCorrectBinding(request, registration)) {
129-
this.logger.trace("Did not process logout request since used incorrect binding");
128+
129+
Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request);
130+
if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) {
131+
this.logger.trace("Did not process logout response since used incorrect binding");
130132
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
131133
return;
132134
}
133135

134136
String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE);
135137
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
136138
.samlResponse(serialized).relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE))
137-
.binding(registration.getSingleLogoutServiceBinding())
138-
.location(registration.getSingleLogoutServiceResponseLocation())
139+
.binding(saml2MessageBinding).location(registration.getSingleLogoutServiceResponseLocation())
139140
.parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG,
140141
request.getParameter(Saml2ParameterNames.SIG_ALG)))
141142
.parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE,
@@ -167,12 +168,4 @@ public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutReques
167168
this.logoutRequestRepository = logoutRequestRepository;
168169
}
169170

170-
private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) {
171-
Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding();
172-
if (requiredBinding == Saml2MessageBinding.POST) {
173-
return "POST".equals(request.getMethod());
174-
}
175-
return "GET".equals(request.getMethod());
176-
}
177-
178171
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.provider.service.web.authentication.logout;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
21+
import org.springframework.security.saml2.Saml2Exception;
22+
import org.springframework.security.saml2.core.Saml2ParameterNames;
23+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
24+
25+
/**
26+
* Utility methods for working with {@link Saml2MessageBinding}
27+
*
28+
* For internal use only.
29+
*
30+
* @since 5.8
31+
*/
32+
final class Saml2MessageBindingUtils {
33+
34+
private Saml2MessageBindingUtils() {
35+
}
36+
37+
static Saml2MessageBinding resolveBinding(HttpServletRequest request) {
38+
if (isHttpPostBinding(request)) {
39+
return Saml2MessageBinding.POST;
40+
}
41+
else if (isHttpRedirectBinding(request)) {
42+
return Saml2MessageBinding.REDIRECT;
43+
}
44+
throw new Saml2Exception("Unable to determine message binding from request.");
45+
}
46+
47+
private static boolean isSamlRequestResponse(HttpServletRequest request) {
48+
return (request.getParameter(Saml2ParameterNames.SAML_REQUEST) != null
49+
|| request.getParameter(Saml2ParameterNames.SAML_RESPONSE) != null);
50+
}
51+
52+
static boolean isHttpRedirectBinding(HttpServletRequest request) {
53+
return request != null && "GET".equalsIgnoreCase(request.getMethod()) && isSamlRequestResponse(request);
54+
}
55+
56+
static boolean isHttpPostBinding(HttpServletRequest request) {
57+
return request != null && "POST".equalsIgnoreCase(request.getMethod()) && isSamlRequestResponse(request);
58+
}
59+
60+
}

0 commit comments

Comments
 (0)