Skip to content

Commit

Permalink
Add properties for SAML relying party single logout
Browse files Browse the repository at this point in the history
Closes gh-30128

Co-authored-by: Madhura Bhave <bhavem@vmware.com>
  • Loading branch information
scottfrederick and mbhave committed May 17, 2022
1 parent 179e372 commit 7d459a1
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 17 deletions.
Expand Up @@ -38,6 +38,7 @@ class Saml2LoginConfiguration {
@Bean
SecurityFilterChain samlSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().authenticated()).saml2Login();
http.saml2Logout();
return http.build();
}

Expand Down
Expand Up @@ -65,6 +65,8 @@ public static class Registration {

private final Decryption decryption = new Decryption();

private final Singlelogout singlelogout = new Singlelogout();

/**
* Remote SAML Identity Provider.
*/
Expand Down Expand Up @@ -111,6 +113,10 @@ public AssertingParty getIdentityprovider() {
return this.identityprovider;
}

public Singlelogout getSinglelogout() {
return this.singlelogout;
}

public static class Acs {

/**
Expand Down Expand Up @@ -258,6 +264,8 @@ public static class AssertingParty {

private final Verification verification = new Verification();

private final Singlelogout singlelogout = new Singlelogout();

public String getEntityId() {
return this.entityId;
}
Expand All @@ -282,6 +290,10 @@ public Verification getVerification() {
return this.verification;
}

public Singlelogout getSinglelogout() {
return this.singlelogout;
}

/**
* Single sign on details for an Identity Provider.
*/
Expand Down Expand Up @@ -372,4 +384,50 @@ public void setCertificateLocation(Resource certificate) {

}

/**
* Single logout details.
*/
public static class Singlelogout {

/**
* Location where SAML2 LogoutRequest gets sent to.
*/
private String url;

/**
* Location where SAML2 LogoutResponse gets sent to.
*/
private String responseUrl;

/**
* Whether to redirect or post logout requests.
*/
private Saml2MessageBinding binding;

public String getUrl() {
return this.url;
}

public void setUrl(String url) {
this.url = url;
}

public String getResponseUrl() {
return this.responseUrl;
}

public void setResponseUrl(String responseUrl) {
this.responseUrl = responseUrl;
}

public Saml2MessageBinding getBinding() {
return this.binding;
}

public void setBinding(Saml2MessageBinding binding) {
this.binding = binding;
}

}

}
Expand Up @@ -96,6 +96,9 @@ private RelyingPartyRegistration asRegistration(String id, Registration properti
builder.assertingPartyDetails(
(details) -> details.verificationX509Credentials((credentials) -> assertingParty.getVerification()
.getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add)));
builder.singleLogoutServiceLocation(properties.getSinglelogout().getUrl());
builder.singleLogoutServiceResponseLocation(properties.getSinglelogout().getResponseUrl());
builder.singleLogoutServiceBinding(properties.getSinglelogout().getBinding());
builder.entityId(properties.getEntityId());
RelyingPartyRegistration registration = builder.build();
boolean signRequest = registration.getAssertingPartyDetails().getWantAuthnRequestsSigned();
Expand All @@ -113,6 +116,9 @@ private Consumer<AssertingPartyDetails.Builder> mapAssertingParty(Registration r
map.from(assertingParty::getSingleSignonUrl).to(details::singleSignOnServiceLocation);
map.from(assertingParty::getSingleSignonSignRequest).when((ignored) -> !usingMetadata)
.to(details::wantAuthnRequestsSigned);
map.from(assertingParty.getSinglelogoutUrl()).to(details::singleLogoutServiceLocation);
map.from(assertingParty.getSinglelogoutResponseUrl()).to(details::singleLogoutServiceResponseLocation);
map.from(assertingParty.getSinglelogoutBinding()).to(details::singleLogoutServiceBinding);
};
}

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

String getSinglelogoutUrl() {
return this.registration.getAssertingparty().getSinglelogout().getUrl();
}

String getSinglelogoutResponseUrl() {
return this.registration.getAssertingparty().getSinglelogout().getResponseUrl();
}

Saml2MessageBinding getSinglelogoutBinding() {
return this.registration.getAssertingparty().getSinglelogout().getBinding();
}

@SuppressWarnings("deprecation")
private <T> T get(String name, Function<AssertingParty, T> getter) {
T newValue = getter.apply(this.registration.getAssertingparty());
Expand Down
Expand Up @@ -43,6 +43,7 @@
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;

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

@Test
@Deprecated
void autoConfigurationShouldBeConditionalOnRelyingPartyRegistrationRepositoryClassDeprecated() {
this.contextRunner.withPropertyValues(getPropertyValues(true)).withClassLoader(new FilteredClassLoader(
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"))
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"))
.run((context) -> assertThat(context).doesNotHaveBean(RelyingPartyRegistrationRepository.class));
}

Expand Down Expand Up @@ -144,6 +145,17 @@ void relyingPartyRegistrationRepositoryBeanShouldBeCreatedWhenPropertiesPresentD
assertThat(registration.getDecryptionX509Credentials()).hasSize(1);
assertThat(registration.getAssertingPartyDetails().getVerificationX509Credentials()).isNotNull();
assertThat(registration.getEntityId()).isEqualTo("{baseUrl}/saml2/foo-entity-id");
assertThat(registration.getSingleLogoutServiceLocation())
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php");
assertThat(registration.getSingleLogoutServiceResponseLocation())
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/");
assertThat(registration.getSingleLogoutServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php");
assertThat(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
.isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/");
assertThat(registration.getAssertingPartyDetails().getSingleLogoutServiceBinding())
.isEqualTo(Saml2MessageBinding.POST);
});
}

Expand Down Expand Up @@ -252,12 +264,12 @@ void autoconfigurationWhenMetadataUrlAndPropertyPresentShouldUseBindingFromPrope
setupMockResponse(server, new ClassPathResource("saml/idp-metadata"));
this.contextRunner.withPropertyValues(PREFIX + ".foo.assertingparty.metadata-uri=" + metadataUrl,
PREFIX + ".foo.assertingparty.singlesignon.binding=redirect").run((context) -> {
RelyingPartyRegistrationRepository repository = context
.getBean(RelyingPartyRegistrationRepository.class);
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.isEqualTo(Saml2MessageBinding.REDIRECT);
});
RelyingPartyRegistrationRepository repository = context
.getBean(RelyingPartyRegistrationRepository.class);
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.isEqualTo(Saml2MessageBinding.REDIRECT);
});
}
}

Expand All @@ -270,12 +282,12 @@ void autoconfigurationWhenMetadataUrlAndPropertyPresentShouldUseBindingFromPrope
setupMockResponse(server, new ClassPathResource("saml/idp-metadata"));
this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl,
PREFIX + ".foo.identityprovider.singlesignon.binding=redirect").run((context) -> {
RelyingPartyRegistrationRepository repository = context
.getBean(RelyingPartyRegistrationRepository.class);
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.isEqualTo(Saml2MessageBinding.REDIRECT);
});
RelyingPartyRegistrationRepository repository = context
.getBean(RelyingPartyRegistrationRepository.class);
RelyingPartyRegistration registration = repository.findByRegistrationId("foo");
assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.isEqualTo(Saml2MessageBinding.REDIRECT);
});
}
}

Expand Down Expand Up @@ -377,6 +389,12 @@ void samlLoginShouldShouldBeConditionalOnSecurityWebFilterClassDeprecated() {
.run((context) -> assertThat(context).doesNotHaveBean(SecurityFilterChain.class));
}

@Test
void samlLogoutShouldBeConfigured() {
this.contextRunner.withPropertyValues(getPropertyValues(false))
.run((context) -> assertThat(hasFilter(context, Saml2LogoutRequestFilter.class)).isTrue());
}

private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests, boolean useDeprecated) {
String assertingParty = useDeprecated ? "identityprovider" : "assertingparty";
return new String[] {
Expand All @@ -387,7 +405,7 @@ private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests
PREFIX + ".foo." + assertingParty
+ ".entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php",
PREFIX + ".foo." + assertingParty
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location" };
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location"};
}

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

private String[] getPropertyValues(boolean useDeprecated) {
Expand All @@ -409,6 +427,9 @@ private String[] getPropertyValues(boolean useDeprecated) {
PREFIX + ".foo.signing.credentials[0].certificate-location=classpath:saml/certificate-location",
PREFIX + ".foo.decryption.credentials[0].private-key-location=classpath:saml/private-key-location",
PREFIX + ".foo.decryption.credentials[0].certificate-location=classpath:saml/certificate-location",
PREFIX + ".foo.singlelogout.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php",
PREFIX + ".foo.singlelogout.response-url=https://simplesaml-for-spring-saml.cfapps.io/",
PREFIX + ".foo.singlelogout.binding=post",
PREFIX + ".foo." + assertingParty
+ ".singlesignon.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php",
PREFIX + ".foo." + assertingParty + ".singlesignon.binding=post",
Expand All @@ -417,9 +438,12 @@ private String[] getPropertyValues(boolean useDeprecated) {
+ ".entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php",
PREFIX + ".foo." + assertingParty
+ ".verification.credentials[0].certificate-location=classpath:saml/certificate-location",
PREFIX + ".foo.asserting-party.singlelogout.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SLOService.php",
PREFIX + ".foo.asserting-party.singlelogout.response-url=https://simplesaml-for-spring-saml.cfapps.io/",
PREFIX + ".foo.asserting-party.singlelogout.binding=post",
PREFIX + ".foo.entity-id={baseUrl}/saml2/foo-entity-id",
PREFIX + ".foo.acs.location={baseUrl}/login/saml2/foo-entity-id",
PREFIX + ".foo.acs.binding=redirect" };
PREFIX + ".foo.acs.binding=redirect"};
}

private boolean hasFilter(AssertableWebApplicationContext context, Class<? extends Filter> filter) {
Expand Down
Expand Up @@ -262,6 +262,10 @@ You can register multiple relying parties under the `spring.security.saml2.relyi
credentials:
- private-key-location: "path-to-private-key"
certificate-location: "path-to-certificate"
singlelogout:
url: "https://myapp/logout/saml2/slo"
reponse-url: "https://remoteidp2.slo.url"
binding: "POST"
assertingparty:
verification:
credentials:
Expand All @@ -284,4 +288,14 @@ You can register multiple relying parties under the `spring.security.saml2.relyi
- certificate-location: "path-to-other-verification-cert"
entity-id: "remote-idp-entity-id2"
sso-url: "https://remoteidp2.sso.url"
singlelogout:
url: "https://remoteidp2.slo.url"
reponse-url: "https://myapp/logout/saml2/slo"
binding: "POST"
----

For SAML2 logout, by default, Spring Security's `Saml2LogoutRequestFilter` and `Saml2LogoutResponseFilter` only process URLs matching `/logout/saml2/slo`.
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.
For example, for servlet applications, you can add your own `SecurityFilterChain` that resembles the following:

include::code:MySamlRelyingPartyConfiguration[]
@@ -0,0 +1,36 @@
/*
* Copyright 2012-2022 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.docs.web.security.saml2.relyingparty;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class MySamlRelyingPartyConfiguration {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.saml2Login();
http.saml2Logout((saml2) -> saml2.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
.logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")));
return http.build();
}

}

0 comments on commit 7d459a1

Please sign in to comment.