Skip to content

Commit 7d459a1

Browse files
Add properties for SAML relying party single logout
Closes gh-30128 Co-authored-by: Madhura Bhave <bhavem@vmware.com>
1 parent 179e372 commit 7d459a1

File tree

6 files changed

+168
-17
lines changed

6 files changed

+168
-17
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2LoginConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Saml2LoginConfiguration {
3838
@Bean
3939
SecurityFilterChain samlSecurityFilterChain(HttpSecurity http) throws Exception {
4040
http.authorizeRequests((requests) -> requests.anyRequest().authenticated()).saml2Login();
41+
http.saml2Logout();
4142
return http.build();
4243
}
4344

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public static class Registration {
6565

6666
private final Decryption decryption = new Decryption();
6767

68+
private final Singlelogout singlelogout = new Singlelogout();
69+
6870
/**
6971
* Remote SAML Identity Provider.
7072
*/
@@ -111,6 +113,10 @@ public AssertingParty getIdentityprovider() {
111113
return this.identityprovider;
112114
}
113115

116+
public Singlelogout getSinglelogout() {
117+
return this.singlelogout;
118+
}
119+
114120
public static class Acs {
115121

116122
/**
@@ -258,6 +264,8 @@ public static class AssertingParty {
258264

259265
private final Verification verification = new Verification();
260266

267+
private final Singlelogout singlelogout = new Singlelogout();
268+
261269
public String getEntityId() {
262270
return this.entityId;
263271
}
@@ -282,6 +290,10 @@ public Verification getVerification() {
282290
return this.verification;
283291
}
284292

293+
public Singlelogout getSinglelogout() {
294+
return this.singlelogout;
295+
}
296+
285297
/**
286298
* Single sign on details for an Identity Provider.
287299
*/
@@ -372,4 +384,50 @@ public void setCertificateLocation(Resource certificate) {
372384

373385
}
374386

387+
/**
388+
* Single logout details.
389+
*/
390+
public static class Singlelogout {
391+
392+
/**
393+
* Location where SAML2 LogoutRequest gets sent to.
394+
*/
395+
private String url;
396+
397+
/**
398+
* Location where SAML2 LogoutResponse gets sent to.
399+
*/
400+
private String responseUrl;
401+
402+
/**
403+
* Whether to redirect or post logout requests.
404+
*/
405+
private Saml2MessageBinding binding;
406+
407+
public String getUrl() {
408+
return this.url;
409+
}
410+
411+
public void setUrl(String url) {
412+
this.url = url;
413+
}
414+
415+
public String getResponseUrl() {
416+
return this.responseUrl;
417+
}
418+
419+
public void setResponseUrl(String responseUrl) {
420+
this.responseUrl = responseUrl;
421+
}
422+
423+
public Saml2MessageBinding getBinding() {
424+
return this.binding;
425+
}
426+
427+
public void setBinding(Saml2MessageBinding binding) {
428+
this.binding = binding;
429+
}
430+
431+
}
432+
375433
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ private RelyingPartyRegistration asRegistration(String id, Registration properti
9696
builder.assertingPartyDetails(
9797
(details) -> details.verificationX509Credentials((credentials) -> assertingParty.getVerification()
9898
.getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add)));
99+
builder.singleLogoutServiceLocation(properties.getSinglelogout().getUrl());
100+
builder.singleLogoutServiceResponseLocation(properties.getSinglelogout().getResponseUrl());
101+
builder.singleLogoutServiceBinding(properties.getSinglelogout().getBinding());
99102
builder.entityId(properties.getEntityId());
100103
RelyingPartyRegistration registration = builder.build();
101104
boolean signRequest = registration.getAssertingPartyDetails().getWantAuthnRequestsSigned();
@@ -113,6 +116,9 @@ private Consumer<AssertingPartyDetails.Builder> mapAssertingParty(Registration r
113116
map.from(assertingParty::getSingleSignonUrl).to(details::singleSignOnServiceLocation);
114117
map.from(assertingParty::getSingleSignonSignRequest).when((ignored) -> !usingMetadata)
115118
.to(details::wantAuthnRequestsSigned);
119+
map.from(assertingParty.getSinglelogoutUrl()).to(details::singleLogoutServiceLocation);
120+
map.from(assertingParty.getSinglelogoutResponseUrl()).to(details::singleLogoutServiceResponseLocation);
121+
map.from(assertingParty.getSinglelogoutBinding()).to(details::singleLogoutServiceBinding);
116122
};
117123
}
118124

@@ -201,6 +207,18 @@ Boolean getSingleSignonSignRequest() {
201207
return get("singlesignon.sign-request", (property) -> property.getSinglesignon().getSignRequest());
202208
}
203209

210+
String getSinglelogoutUrl() {
211+
return this.registration.getAssertingparty().getSinglelogout().getUrl();
212+
}
213+
214+
String getSinglelogoutResponseUrl() {
215+
return this.registration.getAssertingparty().getSinglelogout().getResponseUrl();
216+
}
217+
218+
Saml2MessageBinding getSinglelogoutBinding() {
219+
return this.registration.getAssertingparty().getSinglelogout().getBinding();
220+
}
221+
204222
@SuppressWarnings("deprecation")
205223
private <T> T get(String name, Function<AssertingParty, T> getter) {
206224
T newValue = getter.apply(this.registration.getAssertingparty());

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
4444
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
4545
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
46+
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
4647
import org.springframework.security.web.FilterChainProxy;
4748
import org.springframework.security.web.SecurityFilterChain;
4849

@@ -65,15 +66,15 @@ class Saml2RelyingPartyAutoConfigurationTests {
6566
@Test
6667
void autoConfigurationShouldBeConditionalOnRelyingPartyRegistrationRepositoryClass() {
6768
this.contextRunner.withPropertyValues(getPropertyValues(false)).withClassLoader(new FilteredClassLoader(
68-
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"))
69+
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"))
6970
.run((context) -> assertThat(context).doesNotHaveBean(RelyingPartyRegistrationRepository.class));
7071
}
7172

7273
@Test
7374
@Deprecated
7475
void autoConfigurationShouldBeConditionalOnRelyingPartyRegistrationRepositoryClassDeprecated() {
7576
this.contextRunner.withPropertyValues(getPropertyValues(true)).withClassLoader(new FilteredClassLoader(
76-
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"))
77+
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"))
7778
.run((context) -> assertThat(context).doesNotHaveBean(RelyingPartyRegistrationRepository.class));
7879
}
7980

@@ -144,6 +145,17 @@ void relyingPartyRegistrationRepositoryBeanShouldBeCreatedWhenPropertiesPresentD
144145
assertThat(registration.getDecryptionX509Credentials()).hasSize(1);
145146
assertThat(registration.getAssertingPartyDetails().getVerificationX509Credentials()).isNotNull();
146147
assertThat(registration.getEntityId()).isEqualTo("{baseUrl}/saml2/foo-entity-id");
148+
assertThat(registration.getSingleLogoutServiceLocation())
149+
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php");
150+
assertThat(registration.getSingleLogoutServiceResponseLocation())
151+
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/");
152+
assertThat(registration.getSingleLogoutServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
153+
assertThat(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
154+
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php");
155+
assertThat(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
156+
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/");
157+
assertThat(registration.getAssertingPartyDetails().getSingleLogoutServiceBinding())
158+
.isEqualTo(Saml2MessageBinding.POST);
147159
});
148160
}
149161

@@ -252,12 +264,12 @@ void autoconfigurationWhenMetadataUrlAndPropertyPresentShouldUseBindingFromPrope
252264
setupMockResponse(server, new ClassPathResource("saml/idp-metadata"));
253265
this.contextRunner.withPropertyValues(PREFIX + ".foo.assertingparty.metadata-uri=" + metadataUrl,
254266
PREFIX + ".foo.assertingparty.singlesignon.binding=redirect").run((context) -> {
255-
RelyingPartyRegistrationRepository repository = context
256-
.getBean(RelyingPartyRegistrationRepository.class);
257-
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
258-
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
259-
.isEqualTo(Saml2MessageBinding.REDIRECT);
260-
});
267+
RelyingPartyRegistrationRepository repository = context
268+
.getBean(RelyingPartyRegistrationRepository.class);
269+
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
270+
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
271+
.isEqualTo(Saml2MessageBinding.REDIRECT);
272+
});
261273
}
262274
}
263275

@@ -270,12 +282,12 @@ void autoconfigurationWhenMetadataUrlAndPropertyPresentShouldUseBindingFromPrope
270282
setupMockResponse(server, new ClassPathResource("saml/idp-metadata"));
271283
this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl,
272284
PREFIX + ".foo.identityprovider.singlesignon.binding=redirect").run((context) -> {
273-
RelyingPartyRegistrationRepository repository = context
274-
.getBean(RelyingPartyRegistrationRepository.class);
275-
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
276-
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
277-
.isEqualTo(Saml2MessageBinding.REDIRECT);
278-
});
285+
RelyingPartyRegistrationRepository repository = context
286+
.getBean(RelyingPartyRegistrationRepository.class);
287+
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
288+
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
289+
.isEqualTo(Saml2MessageBinding.REDIRECT);
290+
});
279291
}
280292
}
281293

@@ -377,6 +389,12 @@ void samlLoginShouldShouldBeConditionalOnSecurityWebFilterClassDeprecated() {
377389
.run((context) -> assertThat(context).doesNotHaveBean(SecurityFilterChain.class));
378390
}
379391

392+
@Test
393+
void samlLogoutShouldBeConfigured() {
394+
this.contextRunner.withPropertyValues(getPropertyValues(false))
395+
.run((context) -> assertThat(hasFilter(context, Saml2LogoutRequestFilter.class)).isTrue());
396+
}
397+
380398
private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests, boolean useDeprecated) {
381399
String assertingParty = useDeprecated ? "identityprovider" : "assertingparty";
382400
return new String[] {
@@ -387,7 +405,7 @@ private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests
387405
PREFIX + ".foo." + assertingParty
388406
+ ".entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php",
389407
PREFIX + ".foo." + assertingParty
390-
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location" };
408+
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location"};
391409
}
392410

393411
private String[] getPropertyValuesWithoutSsoBinding(boolean useDeprecated) {
@@ -399,7 +417,7 @@ private String[] getPropertyValuesWithoutSsoBinding(boolean useDeprecated) {
399417
PREFIX + ".foo." + assertingParty
400418
+ ".entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php",
401419
PREFIX + ".foo." + assertingParty
402-
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location" };
420+
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location"};
403421
}
404422

405423
private String[] getPropertyValues(boolean useDeprecated) {
@@ -409,6 +427,9 @@ private String[] getPropertyValues(boolean useDeprecated) {
409427
PREFIX + ".foo.signing.credentials[0].certificate-location=classpath:saml/certificate-location",
410428
PREFIX + ".foo.decryption.credentials[0].private-key-location=classpath:saml/private-key-location",
411429
PREFIX + ".foo.decryption.credentials[0].certificate-location=classpath:saml/certificate-location",
430+
PREFIX + ".foo.singlelogout.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php",
431+
PREFIX + ".foo.singlelogout.response-url=https://simplesaml-for-spring-saml.cfapps.io/",
432+
PREFIX + ".foo.singlelogout.binding=post",
412433
PREFIX + ".foo." + assertingParty
413434
+ ".singlesignon.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php",
414435
PREFIX + ".foo." + assertingParty + ".singlesignon.binding=post",
@@ -417,9 +438,12 @@ private String[] getPropertyValues(boolean useDeprecated) {
417438
+ ".entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php",
418439
PREFIX + ".foo." + assertingParty
419440
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location",
441+
PREFIX + ".foo.asserting-party.singlelogout.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php",
442+
PREFIX + ".foo.asserting-party.singlelogout.response-url=https://simplesaml-for-spring-saml.cfapps.io/",
443+
PREFIX + ".foo.asserting-party.singlelogout.binding=post",
420444
PREFIX + ".foo.entity-id={baseUrl}/saml2/foo-entity-id",
421445
PREFIX + ".foo.acs.location={baseUrl}/login/saml2/foo-entity-id",
422-
PREFIX + ".foo.acs.binding=redirect" };
446+
PREFIX + ".foo.acs.binding=redirect"};
423447
}
424448

425449
private boolean hasFilter(AssertableWebApplicationContext context, Class<? extends Filter> filter) {

spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ You can register multiple relying parties under the `spring.security.saml2.relyi
262262
credentials:
263263
- private-key-location: "path-to-private-key"
264264
certificate-location: "path-to-certificate"
265+
singlelogout:
266+
url: "https://myapp/logout/saml2/slo"
267+
reponse-url: "https://remoteidp2.slo.url"
268+
binding: "POST"
265269
assertingparty:
266270
verification:
267271
credentials:
@@ -284,4 +288,14 @@ You can register multiple relying parties under the `spring.security.saml2.relyi
284288
- certificate-location: "path-to-other-verification-cert"
285289
entity-id: "remote-idp-entity-id2"
286290
sso-url: "https://remoteidp2.sso.url"
291+
singlelogout:
292+
url: "https://remoteidp2.slo.url"
293+
reponse-url: "https://myapp/logout/saml2/slo"
294+
binding: "POST"
287295
----
296+
297+
For SAML2 logout, by default, Spring Security's `Saml2LogoutRequestFilter` and `Saml2LogoutResponseFilter` only process URLs matching `/logout/saml2/slo`.
298+
If you want to customize the `url` to which AP-initiated logout requests get sent to or the `response-url` to which an AP sends logout responses to, to use a different pattern, you need to provide configuration to process that custom pattern.
299+
For example, for servlet applications, you can add your own `SecurityFilterChain` that resembles the following:
300+
301+
include::code:MySamlRelyingPartyConfiguration[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2012-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.boot.docs.web.security.saml2.relyingparty;
18+
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
22+
import org.springframework.security.web.SecurityFilterChain;
23+
24+
@Configuration(proxyBeanMethods = false)
25+
public class MySamlRelyingPartyConfiguration {
26+
27+
@Bean
28+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
29+
http.authorizeRequests().anyRequest().authenticated();
30+
http.saml2Login();
31+
http.saml2Logout((saml2) -> saml2.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
32+
.logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")));
33+
return http.build();
34+
}
35+
36+
}

0 commit comments

Comments
 (0)