Browse Source

SAML 2.0 SP Metadata Endpoint Support

Issue gh-8693
Jakub Kubrynski 5 years ago
parent
commit
8a355240bc
14 changed files with 518 additions and 20 deletions
  1. 3 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java
  2. 27 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java
  3. 1 1
      config/src/test/kotlin/org/springframework/security/config/web/servlet/Saml2DslTests.kt
  4. 3 15
      docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc
  5. 28 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java
  6. 161 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlMetadataResolver.java
  7. 81 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java
  8. 29 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataResolver.java
  9. 1 1
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java
  10. 1 1
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java
  11. 67 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/OpenSamlMetadataResolverTest.java
  12. 114 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTest.java
  13. 1 1
      samples/javaconfig/saml2login/src/main/java/org/springframework/security/samples/config/SecurityConfig.java
  14. 1 1
      samples/javaconfig/saml2login/src/test/java/org/springframework/security/samples/config/SecurityConfigTests.java

+ 3 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java

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

+ 27 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java

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

+ 1 - 1
config/src/test/kotlin/org/springframework/security/config/web/servlet/Saml2DslTests.kt

@@ -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.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION
 import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository
 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.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.MockMvc
 import org.springframework.test.web.servlet.get
 import org.springframework.test.web.servlet.get
 import java.security.cert.Certificate
 import java.security.cert.Certificate

+ 3 - 15
docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc

@@ -61,8 +61,7 @@ the IDP sends an assertion to the SP.
 
 
 1. Mappings assertion conditions and attributes to session features (timeout, tracking, etc)
 1. Mappings assertion conditions and attributes to session features (timeout, tracking, etc)
 2. Single logout
 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]]
 [[servlet-saml2-javaconfig]]
 === Saml 2 Login - Introduction to Java Configuration
 === Saml 2 Login - Introduction to Java Configuration
@@ -200,19 +199,8 @@ credentials on all the identity providers.
 [[servlet-saml2-serviceprovider-metadata]]
 [[servlet-saml2-serviceprovider-metadata]]
 ==== Service Provider 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]]
 [[servlet-saml2-sp-initiated]]
 ==== Authentication Requests - SP Initiated Flow
 ==== Authentication Requests - SP Initiated Flow

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

@@ -29,6 +29,7 @@ import java.util.function.Function;
 
 
 import org.springframework.security.saml2.core.Saml2X509Credential;
 import org.springframework.security.saml2.core.Saml2X509Credential;
 import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
 import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
+import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationFilter;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
 /**
 /**
@@ -360,6 +361,7 @@ public class RelyingPartyRegistration {
 					.encryptionX509Credentials(c -> c.addAll(registration.getAssertingPartyDetails().getEncryptionX509Credentials()))
 					.encryptionX509Credentials(c -> c.addAll(registration.getAssertingPartyDetails().getEncryptionX509Credentials()))
 					.singleSignOnServiceLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
 					.singleSignOnServiceLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
 					.singleSignOnServiceBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
 					.singleSignOnServiceBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
+					.nameIdFormat(registration.getAssertingPartyDetails().getNameIdFormat())
 				);
 				);
 	}
 	}
 
 
@@ -375,6 +377,7 @@ public class RelyingPartyRegistration {
 		private final Collection<Saml2X509Credential> verificationX509Credentials;
 		private final Collection<Saml2X509Credential> verificationX509Credentials;
 		private final Collection<Saml2X509Credential> encryptionX509Credentials;
 		private final Collection<Saml2X509Credential> encryptionX509Credentials;
 		private final String singleSignOnServiceLocation;
 		private final String singleSignOnServiceLocation;
+		private final String nameIdFormat;
 		private final Saml2MessageBinding singleSignOnServiceBinding;
 		private final Saml2MessageBinding singleSignOnServiceBinding;
 
 
 		private AssertingPartyDetails(
 		private AssertingPartyDetails(
@@ -383,6 +386,7 @@ public class RelyingPartyRegistration {
 				Collection<Saml2X509Credential> verificationX509Credentials,
 				Collection<Saml2X509Credential> verificationX509Credentials,
 				Collection<Saml2X509Credential> encryptionX509Credentials,
 				Collection<Saml2X509Credential> encryptionX509Credentials,
 				String singleSignOnServiceLocation,
 				String singleSignOnServiceLocation,
+				String nameIdFormat,
 				Saml2MessageBinding singleSignOnServiceBinding) {
 				Saml2MessageBinding singleSignOnServiceBinding) {
 
 
 			Assert.hasText(entityId, "entityId cannot be null or empty");
 			Assert.hasText(entityId, "entityId cannot be null or empty");
@@ -405,6 +409,7 @@ public class RelyingPartyRegistration {
 			this.verificationX509Credentials = verificationX509Credentials;
 			this.verificationX509Credentials = verificationX509Credentials;
 			this.encryptionX509Credentials = encryptionX509Credentials;
 			this.encryptionX509Credentials = encryptionX509Credentials;
 			this.singleSignOnServiceLocation = singleSignOnServiceLocation;
 			this.singleSignOnServiceLocation = singleSignOnServiceLocation;
+			this.nameIdFormat = nameIdFormat;
 			this.singleSignOnServiceBinding = singleSignOnServiceBinding;
 			this.singleSignOnServiceBinding = singleSignOnServiceBinding;
 		}
 		}
 
 
@@ -472,6 +477,15 @@ public class RelyingPartyRegistration {
 			return this.singleSignOnServiceLocation;
 			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
 		 * Get the
 		 * <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
 		 * <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> verificationX509Credentials = new HashSet<>();
 			private Collection<Saml2X509Credential> encryptionX509Credentials = new HashSet<>();
 			private Collection<Saml2X509Credential> encryptionX509Credentials = new HashSet<>();
 			private String singleSignOnServiceLocation;
 			private String singleSignOnServiceLocation;
+			private String nameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
 			private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
 			private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
 
 
 			/**
 			/**
@@ -562,6 +577,18 @@ public class RelyingPartyRegistration {
 				return this;
 				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
 			 * Set the
 			 * <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-SingleSign-OnServices">SingleSignOnService</a>
 			 * <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.verificationX509Credentials,
 						this.encryptionX509Credentials,
 						this.encryptionX509Credentials,
 						this.singleSignOnServiceLocation,
 						this.singleSignOnServiceLocation,
+						this.nameIdFormat,
 						this.singleSignOnServiceBinding
 						this.singleSignOnServiceBinding
 				);
 				);
 			}
 			}

+ 161 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlMetadataResolver.java

@@ -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);
+	}
+
+}

+ 81 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java

@@ -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);
+	}
+
+}

+ 29 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataResolver.java

@@ -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);
+}

+ 1 - 1
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java

@@ -18,7 +18,7 @@ package org.springframework.security.saml2.provider.service.registration;
 
 
 import org.junit.Test;
 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.assertj.core.api.Assertions.assertThat;
 import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential;
 import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential;

+ 1 - 1
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java

@@ -17,7 +17,7 @@
 package org.springframework.security.saml2.provider.service.registration;
 package org.springframework.security.saml2.provider.service.registration;
 
 
 import org.springframework.security.saml2.credentials.Saml2X509Credential;
 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.relyingPartySigningCredential;
 import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential;
 import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential;

+ 67 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/OpenSamlMetadataResolverTest.java

@@ -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\"");
+	}
+
+}

+ 114 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTest.java

@@ -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);
+	}
+
+}

+ 1 - 1
samples/javaconfig/saml2login/src/main/java/org/springframework/security/samples/config/SecurityConfig.java

@@ -29,7 +29,7 @@ import org.springframework.security.converter.RsaKeyConverters;
 import org.springframework.security.saml2.core.Saml2X509Credential;
 import org.springframework.security.saml2.core.Saml2X509Credential;
 import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
 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.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.DECRYPTION;
 import static org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType.SIGNING;
 import static org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType.SIGNING;

+ 1 - 1
samples/javaconfig/saml2login/src/test/java/org/springframework/security/samples/config/SecurityConfigTests.java

@@ -17,7 +17,7 @@ package org.springframework.security.samples.config;
 
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
 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.security.web.FilterChainProxy;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;