SAML 2.0 SP Metadata Endpoint Support

Issue gh-8693
This commit is contained in:
Jakub Kubrynski 2020-06-30 23:06:32 +02:00 committed by Josh Cummings
parent 31bae546e2
commit 8a355240bc
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
14 changed files with 518 additions and 20 deletions

View File

@ -73,6 +73,9 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter",
order.next());
filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());

View File

@ -38,8 +38,11 @@ import org.springframework.security.saml2.provider.service.servlet.filter.Saml2W
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.DefaultSaml2AuthenticationRequestContextResolver;
import org.springframework.security.saml2.provider.service.web.OpenSamlMetadataResolver;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
import org.springframework.security.saml2.provider.service.web.Saml2MetadataResolver;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
@ -110,10 +113,15 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private AuthenticationConverter authenticationConverter;
private Saml2MetadataResolver saml2MetadataResolver;
private AuthenticationManager authenticationManager;
private Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter;
private Saml2MetadataFilter saml2MetadataFilter;
/**
* Use this {@link AuthenticationConverter} when converting incoming requests to an {@link Authentication}.
* By default the {@link Saml2AuthenticationTokenConverter} is used.
@ -154,6 +162,16 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend
return this;
}
/**
* Sets the {@code Saml2MetadataResolver}
* @param saml2MetadataResolver the implementation of the metadata resolver
* @return the {@link Saml2LoginConfigurer} for further configuration
*/
public Saml2LoginConfigurer saml2MetadataResolver(Saml2MetadataResolver saml2MetadataResolver) {
this.saml2MetadataResolver = saml2MetadataResolver;
return this;
}
/**
* {@inheritDoc}
*/
@ -211,6 +229,14 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend
setAuthenticationFilter(saml2WebSsoAuthenticationFilter);
super.loginProcessingUrl(this.loginProcessingUrl);
if (this.saml2MetadataResolver == null) {
this.saml2MetadataResolver = new OpenSamlMetadataResolver();
}
saml2MetadataFilter = new Saml2MetadataFilter(
this.relyingPartyRegistrationRepository, this.saml2MetadataResolver
);
if (hasText(this.loginPage)) {
// Set custom login page
super.loginPage(this.loginPage);
@ -250,6 +276,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend
@Override
public void configure(B http) throws Exception {
http.addFilter(this.authenticationRequestEndpoint.build(http));
http.addFilter(saml2MetadataFilter);
super.configure(http);
if (this.authenticationManager == null) {
registerDefaultAuthenticationProvider(http);

View File

@ -30,7 +30,7 @@ import org.springframework.security.saml2.credentials.Saml2X509Credential
import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import java.security.cert.Certificate

View File

@ -61,8 +61,7 @@ the IDP sends an assertion to the SP.
1. Mappings assertion conditions and attributes to session features (timeout, tracking, etc)
2. Single logout
3. Dynamic metadata generation
4. Receiving and validating standalone assertion (not wrapped in a response object)
3. Receiving and validating standalone assertion (not wrapped in a response object)
[[servlet-saml2-javaconfig]]
=== Saml 2 Login - Introduction to Java Configuration
@ -200,19 +199,8 @@ credentials on all the identity providers.
[[servlet-saml2-serviceprovider-metadata]]
==== Service Provider Metadata
The Spring Security SAML 2 implementation does not yet provide an endpoint for downloading
SP metadata in XML format. The minimal pieces that are exchanged
* *entity ID* - defaults to `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`
Other known configuration names that also use this same value
** Audience Restriction
* *single signon URL* - defaults to `+{baseUrl}/login/saml2/sso/{registrationId}+`
Other known configuration names that also use this same value
** Recipient URL
** Destination URL
** Assertion Consumer Service URL
* X509Certificate - the certificate that you configure as part of your {SIGNING,DECRYPTION}
credentials must be shared with the Identity Provider
The Spring Security SAML 2 implementation does provide an endpoint for downloading
SP metadata in XML format. The provider is mapped to: `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`
[[servlet-saml2-sp-initiated]]
==== Authentication Requests - SP Initiated Flow

View File

@ -29,6 +29,7 @@ import java.util.function.Function;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
import org.springframework.util.Assert;
/**
@ -360,6 +361,7 @@ public class RelyingPartyRegistration {
.encryptionX509Credentials(c -> c.addAll(registration.getAssertingPartyDetails().getEncryptionX509Credentials()))
.singleSignOnServiceLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
.singleSignOnServiceBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.nameIdFormat(registration.getAssertingPartyDetails().getNameIdFormat())
);
}
@ -375,6 +377,7 @@ public class RelyingPartyRegistration {
private final Collection<Saml2X509Credential> verificationX509Credentials;
private final Collection<Saml2X509Credential> encryptionX509Credentials;
private final String singleSignOnServiceLocation;
private final String nameIdFormat;
private final Saml2MessageBinding singleSignOnServiceBinding;
private AssertingPartyDetails(
@ -383,6 +386,7 @@ public class RelyingPartyRegistration {
Collection<Saml2X509Credential> verificationX509Credentials,
Collection<Saml2X509Credential> encryptionX509Credentials,
String singleSignOnServiceLocation,
String nameIdFormat,
Saml2MessageBinding singleSignOnServiceBinding) {
Assert.hasText(entityId, "entityId cannot be null or empty");
@ -405,6 +409,7 @@ public class RelyingPartyRegistration {
this.verificationX509Credentials = verificationX509Credentials;
this.encryptionX509Credentials = encryptionX509Credentials;
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
this.nameIdFormat = nameIdFormat;
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
}
@ -472,6 +477,15 @@ public class RelyingPartyRegistration {
return this.singleSignOnServiceLocation;
}
/**
* Get the NameIDFormat setting, indicating which user property should be used as a NameID Format attribute
*
* @return the NameIdFormat value
*/
public String getNameIdFormat() {
return nameIdFormat;
}
/**
* Get the
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
@ -493,6 +507,7 @@ public class RelyingPartyRegistration {
private Collection<Saml2X509Credential> verificationX509Credentials = new HashSet<>();
private Collection<Saml2X509Credential> encryptionX509Credentials = new HashSet<>();
private String singleSignOnServiceLocation;
private String nameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
/**
@ -562,6 +577,18 @@ public class RelyingPartyRegistration {
return this;
}
/**
* Set the preference for name identifier returned by IdP.
* See <a href="https://wiki.shibboleth.net/confluence/display/SHIB/NameIdentifierFormat">for possible values</a>
*
* @param nameIdFormat the name identifier
* @return the {@link ProviderDetails.Builder} for further configuration
*/
public Builder nameIdFormat(String nameIdFormat) {
this.nameIdFormat = nameIdFormat;
return this;
}
/**
* Set the
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
@ -590,6 +617,7 @@ public class RelyingPartyRegistration {
this.verificationX509Credentials,
this.encryptionX509Credentials,
this.singleSignOnServiceLocation,
this.nameIdFormat,
this.singleSignOnServiceBinding
);
}

View File

@ -0,0 +1,161 @@
/*
* Copyright 2002-2020 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.security.saml2.provider.service.web;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.opensaml.core.xml.XMLObjectBuilder;
import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.X509Certificate;
import org.opensaml.xmlsec.signature.X509Data;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.credentials.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2ServletUtils;
import org.w3c.dom.Element;
import javax.servlet.http.HttpServletRequest;
import javax.xml.namespace.QName;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
/**
* @author Jakub Kubrynski
* @since 5.4
*/
public class OpenSamlMetadataResolver implements Saml2MetadataResolver {
@Override
public String resolveMetadata(HttpServletRequest request, RelyingPartyRegistration registration) {
XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
EntityDescriptor entityDescriptor = buildObject(builderFactory, EntityDescriptor.ELEMENT_QNAME);
entityDescriptor.setEntityID(
resolveTemplate(registration.getEntityId(), registration, request));
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration, builderFactory, request);
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
return serializeToXmlString(entityDescriptor);
}
private String serializeToXmlString(EntityDescriptor entityDescriptor) {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(entityDescriptor);
if (marshaller == null) {
throw new Saml2Exception("Unable to resolve Marshaller");
}
Element element;
try {
element = marshaller.marshall(entityDescriptor);
} catch (Exception e) {
throw new Saml2Exception(e);
}
return SerializeSupport.prettyPrintXML(element);
}
private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration,
XMLObjectBuilderFactory builderFactory, HttpServletRequest request) {
SPSSODescriptor spSsoDescriptor = buildObject(builderFactory, SPSSODescriptor.DEFAULT_ELEMENT_NAME);
spSsoDescriptor.setAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned());
spSsoDescriptor.setWantAssertionsSigned(true);
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
NameIDFormat nameIdFormat = buildObject(builderFactory, NameIDFormat.DEFAULT_ELEMENT_NAME);
nameIdFormat.setFormat(registration.getAssertingPartyDetails().getNameIdFormat());
spSsoDescriptor.getNameIDFormats().add(nameIdFormat);
spSsoDescriptor.getAssertionConsumerServices().add(
buildAssertionConsumerService(registration, builderFactory, request));
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(builderFactory,
registration.getSigningCredentials(), UsageType.SIGNING));
spSsoDescriptor.getKeyDescriptors().addAll(buildKeys(builderFactory,
registration.getEncryptionCredentials(), UsageType.ENCRYPTION));
return spSsoDescriptor;
}
private List<KeyDescriptor> buildKeys(XMLObjectBuilderFactory builderFactory,
List<Saml2X509Credential> credentials, UsageType usageType) {
List<KeyDescriptor> list = new ArrayList<>();
for (Saml2X509Credential credential : credentials) {
KeyDescriptor keyDescriptor = buildKeyDescriptor(builderFactory, usageType, credential.getCertificate());
list.add(keyDescriptor);
}
return list;
}
private KeyDescriptor buildKeyDescriptor(XMLObjectBuilderFactory builderFactory, UsageType usageType,
java.security.cert.X509Certificate certificate) {
KeyDescriptor keyDescriptor = buildObject(builderFactory, KeyDescriptor.DEFAULT_ELEMENT_NAME);
KeyInfo keyInfo = buildObject(builderFactory, KeyInfo.DEFAULT_ELEMENT_NAME);
X509Certificate x509Certificate = buildObject(builderFactory, X509Certificate.DEFAULT_ELEMENT_NAME);
X509Data x509Data = buildObject(builderFactory, X509Data.DEFAULT_ELEMENT_NAME);
try {
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded())));
} catch (CertificateEncodingException e) {
throw new Saml2Exception("Cannot encode certificate " + certificate.toString());
}
x509Data.getX509Certificates().add(x509Certificate);
keyInfo.getX509Datas().add(x509Data);
keyDescriptor.setUse(usageType);
keyDescriptor.setKeyInfo(keyInfo);
return keyDescriptor;
}
private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration,
XMLObjectBuilderFactory builderFactory, HttpServletRequest request) {
AssertionConsumerService assertionConsumerService = buildObject(builderFactory, AssertionConsumerService.DEFAULT_ELEMENT_NAME);
assertionConsumerService.setLocation(
resolveTemplate(registration.getAssertionConsumerServiceLocation(), registration, request));
assertionConsumerService.setBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding().getUrn());
assertionConsumerService.setIndex(1);
return assertionConsumerService;
}
@SuppressWarnings("unchecked")
private <T> T buildObject(XMLObjectBuilderFactory builderFactory, QName elementName) {
XMLObjectBuilder<?> builder = builderFactory.getBuilder(elementName);
if (builder == null) {
throw new Saml2Exception("Cannot build object - builder not defined for element " + elementName);
}
return (T) builder.buildObject(elementName);
}
private String resolveTemplate(String template, RelyingPartyRegistration registration, HttpServletRequest request) {
return Saml2ServletUtils.resolveUrlTemplate(template, Saml2ServletUtils.getApplicationUri(request), registration);
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2002-2020 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.security.saml2.provider.service.web;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* This {@code Servlet} returns a generated Service Provider Metadata XML
*
* @since 5.4
* @author Jakub Kubrynski
*/
public class Saml2MetadataFilter extends OncePerRequestFilter {
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private final Saml2MetadataResolver saml2MetadataResolver;
private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}");
public Saml2MetadataFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, Saml2MetadataResolver saml2MetadataResolver) {
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
this.saml2MetadataResolver = saml2MetadataResolver;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
RequestMatcher.MatchResult matcher = this.redirectMatcher.matcher(request);
if (!matcher.isMatch()) {
filterChain.doFilter(request, response);
return;
}
String registrationId = matcher.getVariables().get("registrationId");
RelyingPartyRegistration registration = relyingPartyRegistrationRepository.findByRegistrationId(registrationId);
if (registration == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String xml = saml2MetadataResolver.resolveMetadata(request, registration);
writeMetadataToResponse(response, registrationId, xml);
}
private void writeMetadataToResponse(HttpServletResponse response, String registrationId, String xml) throws IOException {
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"saml-" + registrationId + "-metadata.xml\"");
response.setContentLength(xml.length());
response.getWriter().write(xml);
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2002-2020 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.security.saml2.provider.service.web;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import javax.servlet.http.HttpServletRequest;
/**
* @author Jakub Kubrynski
* @since 5.4
*/
public interface Saml2MetadataResolver {
String resolveMetadata(HttpServletRequest request, RelyingPartyRegistration registration);
}

View File

@ -18,7 +18,7 @@ package org.springframework.security.saml2.provider.service.registration;
import org.junit.Test;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential;

View File

@ -17,7 +17,7 @@
package org.springframework.security.saml2.provider.service.registration;
import org.springframework.security.saml2.credentials.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartySigningCredential;
import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential;

View File

@ -0,0 +1,67 @@
/*
* Copyright 2002-2020 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.security.saml2.provider.service.web;
import org.junit.Before;
import org.junit.Test;
import org.opensaml.core.config.InitializationException;
import org.opensaml.core.config.InitializationService;
import org.opensaml.saml.saml2.core.NameIDType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import javax.servlet.http.HttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.REDIRECT;
public class OpenSamlMetadataResolverTest {
@Before
public void setUp() throws InitializationException {
InitializationService.initialize();
}
@Test
public void shouldGenerateMetadata() {
// given
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.relyingPartyRegistration()
.assertingPartyDetails(p -> p.singleSignOnServiceBinding(REDIRECT))
.assertingPartyDetails(p -> p.wantAuthnRequestsSigned(true))
.assertingPartyDetails(p -> p.nameIdFormat(NameIDType.EMAIL))
.build();
HttpServletRequest servletRequestMock = new MockHttpServletRequest();
// when
String metadataXml = openSamlMetadataResolver.resolveMetadata(servletRequestMock, relyingPartyRegistration);
// then
assertThat(metadataXml)
.contains("<EntityDescriptor")
.contains("entityID=\"http://localhost/saml2/service-provider-metadata/simplesamlphp\"")
.contains("AuthnRequestsSigned=\"true\"")
.contains("WantAssertionsSigned=\"true\"")
.contains("<md:KeyDescriptor use=\"signing\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"http://localhost/login/saml2/sso/simplesamlphp\" index=\"1\"");
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright 2002-2020 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.security.saml2.provider.service.web;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import javax.servlet.FilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class Saml2MetadataFilterTest {
RelyingPartyRegistrationRepository repository;
Saml2MetadataResolver saml2MetadataResolver;
Saml2MetadataFilter filter;
MockHttpServletRequest request;
MockHttpServletResponse response;
FilterChain filterChain;
@Before
public void setup() {
repository = mock(RelyingPartyRegistrationRepository.class);
saml2MetadataResolver = mock(Saml2MetadataResolver.class);
filter = new Saml2MetadataFilter(repository, saml2MetadataResolver);
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
filterChain = mock(FilterChain.class);
}
@Test
public void shouldReturnValueWhenMatcherSucceed() throws Exception {
// given
request.setPathInfo("/saml2/service-provider-metadata/registration-id");
// when
filter.doFilter(request, response, filterChain);
// then
verifyNoInteractions(filterChain);
}
@Test
public void shouldProcessFilterChainIfMatcherFails() throws Exception {
// given
request.setPathInfo("/saml2/authenticate/registration-id");
// when
filter.doFilter(request, response, filterChain);
// then
verify(filterChain).doFilter(request, response);
}
@Test
public void shouldReturn401IfNoRegistrationIsFound() throws Exception {
// given
request.setPathInfo("/saml2/service-provider-metadata/invalidRegistration");
when(repository.findByRegistrationId("invalidRegistration")).thenReturn(null);
// when
filter.doFilter(request, response, filterChain);
// then
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(401);
}
@Test
public void shouldInvokeMetadataGenerationIfRegistrationIsFound() throws Exception {
// given
request.setPathInfo("/saml2/service-provider-metadata/validRegistration");
RelyingPartyRegistration validRegistration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
when(repository.findByRegistrationId("validRegistration")).thenReturn(validRegistration);
String generatedMetadata = "<xml>test</xml>";
when(saml2MetadataResolver.resolveMetadata(request, validRegistration)).thenReturn(generatedMetadata);
filter = new Saml2MetadataFilter(repository, saml2MetadataResolver);
// when
filter.doFilter(request, response, filterChain);
// then
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString()).isEqualTo(generatedMetadata);
verify(saml2MetadataResolver).resolveMetadata(request, validRegistration);
}
}

View File

@ -29,7 +29,7 @@ import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
import static org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION;
import static org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType.SIGNING;

View File

@ -17,7 +17,7 @@ package org.springframework.security.samples.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;