Explorar o código

Add saml2Metadata

Closes gh-11828
Josh Cummings %!s(int64=2) %!d(string=hai) anos
pai
achega
46452c0cae

+ 97 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -73,6 +73,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
 import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -2425,6 +2426,102 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
 		return getOrApply(new Saml2LogoutConfigurer<>(getContext()));
 	}
 
+	/**
+	 * Configures a SAML 2.0 metadata endpoint that presents relying party configurations
+	 * in an {@code <md:EntityDescriptor>} payload.
+	 *
+	 * <p>
+	 * By default, the endpoints are {@code /saml2/metadata} and
+	 * {@code /saml2/metadata/{registrationId}} though note that also
+	 * {@code /saml2/service-provider-metadata/{registrationId}} is recognized for
+	 * backward compatibility purposes.
+	 *
+	 * <p>
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The following example shows the minimal configuration required, using a
+	 * hypothetical asserting party.
+	 *
+	 * <pre>
+	 *	&#064;EnableWebSecurity
+	 *	&#064;Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		&#064;Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+	 *				.saml2Metadata(Customizer.withDefaults());
+	 *			return http.build();
+	 *		}
+	 *
+	 *		&#064;Bean
+	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
+	 *					.withMetadataLocation("https://ap.example.org/metadata")
+	 *					.registrationId("simple")
+	 *					.build();
+	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
+	 *		}
+	 *	}
+	 * </pre>
+	 * @param saml2MetadataConfigurer the {@link Customizer} to provide more options for
+	 * the {@link Saml2MetadataConfigurer}
+	 * @return the {@link HttpSecurity} for further customizations
+	 * @throws Exception
+	 * @since 6.1
+	 */
+	public HttpSecurity saml2Metadata(Customizer<Saml2MetadataConfigurer<HttpSecurity>> saml2MetadataConfigurer)
+			throws Exception {
+		saml2MetadataConfigurer.customize(getOrApply(new Saml2MetadataConfigurer<>(getContext())));
+		return HttpSecurity.this;
+	}
+
+	/**
+	 * Configures a SAML 2.0 metadata endpoint that presents relying party configurations
+	 * in an {@code <md:EntityDescriptor>} payload.
+	 *
+	 * <p>
+	 * By default, the endpoints are {@code /saml2/metadata} and
+	 * {@code /saml2/metadata/{registrationId}} though note that also
+	 * {@code /saml2/service-provider-metadata/{registrationId}} is recognized for
+	 * backward compatibility purposes.
+	 *
+	 * <p>
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The following example shows the minimal configuration required, using a
+	 * hypothetical asserting party.
+	 *
+	 * <pre>
+	 *	&#064;EnableWebSecurity
+	 *	&#064;Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		&#064;Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+	 *				.saml2Metadata(Customizer.withDefaults());
+	 *			return http.build();
+	 *		}
+	 *
+	 *		&#064;Bean
+	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
+	 *					.withMetadataLocation("https://ap.example.org/metadata")
+	 *					.registrationId("simple")
+	 *					.build();
+	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
+	 *		}
+	 *	}
+	 * </pre>
+	 * @return the {@link Saml2MetadataConfigurer} for further customizations
+	 * @throws Exception
+	 * @since 6.1
+	 */
+	public Saml2MetadataConfigurer<HttpSecurity> saml2Metadata() throws Exception {
+		return getOrApply(new Saml2MetadataConfigurer<>(getContext()));
+	}
+
 	/**
 	 * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0
 	 * Provider. <br>

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

@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2023 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.config.annotation.web.configurers.saml2;
+
+import java.util.function.Function;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
+import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
+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.web.Saml2MetadataFilter;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AbstractHttpConfigurer} for SAML 2.0 Metadata.
+ *
+ * <p>
+ * SAML 2.0 Metadata provides an application with the capability to publish configuration
+ * information as a {@code <md:EntityDescriptor>} or {@code <md:EntitiesDescriptor>}.
+ *
+ * <p>
+ * Defaults are provided for all configuration options with the only required
+ * configuration being a {@link Saml2LoginConfigurer#relyingPartyRegistrationRepository}.
+ * Alternatively, a {@link RelyingPartyRegistrationRepository} {@code @Bean} may be
+ * registered instead.
+ *
+ * <h2>Security Filters</h2>
+ *
+ * The following {@code Filter} is populated:
+ *
+ * <ul>
+ * <li>{@link Saml2MetadataFilter}</li>
+ * </ul>
+ *
+ * <h2>Shared Objects Created</h2>
+ *
+ * none
+ *
+ * <h2>Shared Objects Used</h2>
+ *
+ * The following shared objects are used:
+ *
+ * <ul>
+ * <li>{@link RelyingPartyRegistrationRepository} (required)</li>
+ * </ul>
+ *
+ * @since 6.1
+ * @see HttpSecurity#saml2Metadata()
+ * @see Saml2MetadataFilter
+ * @see RelyingPartyRegistrationRepository
+ */
+public class Saml2MetadataConfigurer<H extends HttpSecurityBuilder<H>>
+		extends AbstractHttpConfigurer<Saml2LogoutConfigurer<H>, H> {
+
+	private final ApplicationContext context;
+
+	private Function<RelyingPartyRegistrationRepository, Saml2MetadataResponseResolver> metadataResponseResolver;
+
+	public Saml2MetadataConfigurer(ApplicationContext context) {
+		this.context = context;
+	}
+
+	/**
+	 * Use this endpoint to request relying party metadata.
+	 *
+	 * <p>
+	 * If you specify a {@code registrationId} placeholder in the URL, then the filter
+	 * will lookup a {@link RelyingPartyRegistration} using that.
+	 *
+	 * <p>
+	 * If there is no {@code registrationId} and your
+	 * {@link RelyingPartyRegistrationRepository} is {code Iterable}, the metadata
+	 * endpoint will try and show all relying parties' metadata in a single
+	 * {@code <md:EntitiesDecriptor} element.
+	 *
+	 * <p>
+	 * If you need a more sophisticated lookup strategy than these, use
+	 * {@link #metadataResponseResolver} instead.
+	 * @param metadataUrl the url to use
+	 * @return the {@link Saml2MetadataConfigurer} for more customizations
+	 */
+	public Saml2MetadataConfigurer<H> metadataUrl(String metadataUrl) {
+		Assert.hasText(metadataUrl, "metadataUrl cannot be empty");
+		this.metadataResponseResolver = (registrations) -> {
+			RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations,
+					new OpenSamlMetadataResolver());
+			metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl));
+			return metadata;
+		};
+		return this;
+	}
+
+	/**
+	 * Use this {@link Saml2MetadataResponseResolver} to parse the request and respond
+	 * with SAML 2.0 metadata.
+	 * @param metadataResponseResolver to use
+	 * @return the {@link Saml2MetadataConfigurer} for more customizations
+	 */
+	public Saml2MetadataConfigurer<H> metadataResponseResolver(Saml2MetadataResponseResolver metadataResponseResolver) {
+		Assert.notNull(metadataResponseResolver, "metadataResponseResolver cannot be null");
+		this.metadataResponseResolver = (registrations) -> metadataResponseResolver;
+		return this;
+	}
+
+	public H and() {
+		return getBuilder();
+	}
+
+	@Override
+	public void configure(H http) throws Exception {
+		Saml2MetadataResponseResolver metadataResponseResolver = createMetadataResponseResolver(http);
+		http.addFilterBefore(new Saml2MetadataFilter(metadataResponseResolver), BasicAuthenticationFilter.class);
+	}
+
+	private Saml2MetadataResponseResolver createMetadataResponseResolver(H http) {
+		if (this.metadataResponseResolver != null) {
+			RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
+			return this.metadataResponseResolver.apply(registrations);
+		}
+		Saml2MetadataResponseResolver metadataResponseResolver = getBeanOrNull(Saml2MetadataResponseResolver.class);
+		if (metadataResponseResolver != null) {
+			return metadataResponseResolver;
+		}
+		RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
+		return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver());
+	}
+
+	private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {
+		Saml2LoginConfigurer<H> login = http.getConfigurer(Saml2LoginConfigurer.class);
+		if (login != null) {
+			return login.relyingPartyRegistrationRepository(http);
+		}
+		else {
+			return getBeanOrNull(RelyingPartyRegistrationRepository.class);
+		}
+	}
+
+	private <C> C getBeanOrNull(Class<C> clazz) {
+		if (this.context == null) {
+			return null;
+		}
+		if (this.context.getBeanNamesForType(clazz).length == 0) {
+			return null;
+		}
+		return this.context.getBean(clazz);
+	}
+
+}

+ 37 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

@@ -677,6 +677,43 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
         this.http.saml2Login(saml2LoginCustomizer)
     }
 
+	/**
+	 * Configures a SAML 2.0 relying party metadata endpoint.
+	 *
+	 * A [RelyingPartyRegistrationRepository] is required and must be registered with
+	 * the [ApplicationContext] or configured via
+	 * [Saml2Dsl.relyingPartyRegistrationRepository]
+	 *
+	 * Example:
+	 *
+	 * The following example shows the minimal configuration required, using a
+	 * hypothetical asserting party.
+	 *
+	 * ```
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * class SecurityConfig {
+	 *
+	 *     @Bean
+	 *     fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+	 *         http {
+	 *             saml2Login { }
+	 *             saml2Metadata { }
+	 *         }
+	 *         return http.build()
+	 *     }
+	 * }
+	 * ```
+	 * @param saml2MetadataConfiguration custom configuration to configure the
+	 * SAML2 relying party metadata endpoint
+	 * @see [Saml2MetadataDsl]
+	 * @since 6.1
+	 */
+	fun saml2Metadata(saml2MetadataConfiguration: Saml2MetadataDsl.() -> Unit) {
+		val saml2MetadataCustomizer = Saml2MetadataDsl().apply(saml2MetadataConfiguration).get()
+		this.http.saml2Metadata(saml2MetadataCustomizer)
+	}
+
     /**
      * Allows configuring how an anonymous user is represented.
      *

+ 52 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDsl.kt

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-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.security.config.annotation.web
+
+import org.springframework.security.authentication.AuthenticationManagerResolver
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.oauth2.resourceserver.JwtDsl
+import org.springframework.security.config.annotation.web.oauth2.resourceserver.OpaqueTokenDsl
+import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
+import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
+import org.springframework.security.web.AuthenticationEntryPoint
+import org.springframework.security.web.access.AccessDeniedHandler
+import jakarta.servlet.http.HttpServletRequest
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver
+
+/**
+ * A Kotlin DSL to configure [HttpSecurity] SAML 2.0 relying party metadata support using
+ * idiomatic Kotlin code.
+ *
+ * @author Josh Cummings
+ * @since 6.1
+ * @property metadataUrl the name of the relying party metadata endpoint; defaults to `/saml2/metadata` and `/saml2/metadata/{registrationId}`
+ * @property metadataResponseResolver the [Saml2MetadataResponseResolver] to use for resolving the
+ * metadata request into metadata
+ */
+@SecurityMarker
+class Saml2MetadataDsl {
+    var metadataUrl: String? = null
+    var metadataResponseResolver: Saml2MetadataResponseResolver? = null
+
+    internal fun get(): (Saml2MetadataConfigurer<HttpSecurity>) -> Unit {
+        return { saml2Metadata ->
+            metadataResponseResolver?.also { saml2Metadata.metadataResponseResolver(metadataResponseResolver) }
+            metadataUrl?.also { saml2Metadata.metadataUrl(metadataUrl) }
+        }
+    }
+}

+ 210 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java

@@ -0,0 +1,210 @@
+/*
+ * Copyright 2002-2023 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.config.annotation.web.configurers.saml2;
+
+import com.google.common.net.HttpHeaders;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
+import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
+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.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link Saml2MetadataConfigurer}
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class Saml2MetadataConfigurerTests {
+
+	static RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired(required = false)
+	MockMvc mvc;
+
+	@Test
+	void saml2MetadataRegistrationIdWhenDefaultsThenReturnsMetadata() throws Exception {
+		this.spring.register(DefaultConfig.class).autowire();
+		String filename = "saml-" + registration.getRegistrationId() + "-metadata.xml";
+		this.mvc.perform(get("/saml2/metadata/" + registration.getRegistrationId())).andExpect(status().isOk())
+				.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString(filename)))
+				.andExpect(content().string(containsString("md:EntityDescriptor")));
+	}
+
+	@Test
+	void saml2MetadataRegistrationIdWhenWrongIdThenUnauthorized() throws Exception {
+		this.spring.register(DefaultConfig.class).autowire();
+		this.mvc.perform(get("/saml2/metadata/" + registration.getRegistrationId() + "wrong"))
+				.andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void saml2MetadataWhenDefaultsThenReturnsMetadata() throws Exception {
+		this.spring.register(DefaultConfig.class).autowire();
+		this.mvc.perform(get("/saml2/metadata")).andExpect(status().isOk())
+				.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("-metadata.xml")))
+				.andExpect(content().string(containsString("md:EntityDescriptor")));
+	}
+
+	@Test
+	void saml2MetadataWhenMetadataResponseResolverThenUses() throws Exception {
+		this.spring.register(DefaultConfig.class, MetadataResponseResolverConfig.class).autowire();
+		Saml2MetadataResponseResolver metadataResponseResolver = this.spring.getContext()
+				.getBean(Saml2MetadataResponseResolver.class);
+		given(metadataResponseResolver.resolve(any(HttpServletRequest.class)))
+				.willReturn(new Saml2MetadataResponse("metadata", "filename"));
+		this.mvc.perform(get("/saml2/metadata")).andExpect(status().isOk())
+				.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("filename")))
+				.andExpect(content().string(containsString("metadata")));
+		verify(metadataResponseResolver).resolve(any(HttpServletRequest.class));
+	}
+
+	@Test
+	void saml2MetadataWhenMetadataResponseResolverDslThenUses() throws Exception {
+		this.spring.register(MetadataResponseResolverDslConfig.class).autowire();
+		this.mvc.perform(get("/saml2/metadata")).andExpect(status().isOk())
+				.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("filename")))
+				.andExpect(content().string(containsString("metadata")));
+	}
+
+	@Test
+	void saml2MetadataWhenMetadataUrlThenUses() throws Exception {
+		this.spring.register(MetadataUrlConfig.class).autowire();
+		String filename = "saml-" + registration.getRegistrationId() + "-metadata.xml";
+		this.mvc.perform(get("/saml/metadata")).andExpect(status().isOk())
+				.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString(filename)))
+				.andExpect(content().string(containsString("md:EntityDescriptor")));
+		this.mvc.perform(get("/saml2/metadata")).andExpect(status().isForbidden());
+	}
+
+	@EnableWebSecurity
+	@Configuration
+	@Import(RelyingPartyRegistrationConfig.class)
+	static class DefaultConfig {
+
+		@Bean
+		SecurityFilterChain filters(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+				.saml2Metadata(Customizer.withDefaults());
+			return http.build();
+			// @formatter:on
+		}
+
+	}
+
+	@EnableWebSecurity
+	@Configuration
+	@Import(RelyingPartyRegistrationConfig.class)
+	static class MetadataUrlConfig {
+
+		@Bean
+		SecurityFilterChain filters(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+				.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata"));
+			return http.build();
+			// @formatter:on
+		}
+
+		// should ignore
+		@Bean
+		Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) {
+			return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver());
+		}
+
+	}
+
+	@EnableWebSecurity
+	@Configuration
+	@Import(RelyingPartyRegistrationConfig.class)
+	static class MetadataResponseResolverDslConfig {
+
+		Saml2MetadataResponseResolver metadataResponseResolver = mock(Saml2MetadataResponseResolver.class);
+
+		{
+			given(this.metadataResponseResolver.resolve(any(HttpServletRequest.class)))
+					.willReturn(new Saml2MetadataResponse("metadata", "filename"));
+		}
+
+		@Bean
+		SecurityFilterChain filters(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+				.saml2Metadata((saml2) -> saml2.metadataResponseResolver(this.metadataResponseResolver));
+			return http.build();
+			// @formatter:on
+		}
+
+	}
+
+	@Configuration
+	static class MetadataResponseResolverConfig {
+
+		Saml2MetadataResponseResolver metadataResponseResolver = mock(Saml2MetadataResponseResolver.class);
+
+		@Bean
+		Saml2MetadataResponseResolver metadataResponseResolver() {
+			return this.metadataResponseResolver;
+		}
+
+	}
+
+	@Configuration
+	static class RelyingPartyRegistrationConfig {
+
+		RelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository(registration);
+
+		@Bean
+		RelyingPartyRegistrationRepository registrations() {
+			return this.registrations;
+		}
+
+	}
+
+}

+ 189 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDslTests.kt

@@ -0,0 +1,189 @@
+/*
+ * Copyright 2002-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.security.config.annotation.web
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.verify
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.BeanCreationException
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.ClassPathResource
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.authentication.ProviderManager
+import org.springframework.security.authentication.TestingAuthenticationProvider
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.saml2.core.Saml2X509Credential
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver
+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.RelyingPartyRegistrationRepository
+import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations
+import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+import java.util.Base64
+
+/**
+ * Tests for [Saml2Dsl]
+ *
+ * @author Eleftheria Stein
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class Saml2MetadataDslTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `saml2Metadat when no relying party registration repository then exception`() {
+        Assertions.assertThatThrownBy { this.spring.register(Saml2MetadataNoRelyingPartyRegistrationRepoConfig::class.java).autowire() }
+                .isInstanceOf(BeanCreationException::class.java)
+                .hasMessageContaining("relyingPartyRegistrationRepository cannot be null")
+
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    open class Saml2MetadataNoRelyingPartyRegistrationRepoConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                saml2Metadata { }
+            }
+            return http.build()
+        }
+    }
+
+    @Test
+    fun `metadata endpoint when saml2Metadata configured then metadata returned`() {
+        this.spring.register(Saml2MetadataConfig::class.java).autowire()
+
+        this.mockMvc.get("/saml2/metadata")
+                .andExpect {
+                    status { isOk() }
+                }
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    open class Saml2MetadataConfig {
+
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                saml2Metadata { }
+            }
+            return http.build()
+        }
+
+		@Bean
+		open fun registrations(): RelyingPartyRegistrationRepository {
+			return InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build())
+		}
+
+        private fun <T : Certificate> loadCert(location: String): T {
+            ClassPathResource(location).inputStream.use { inputStream ->
+                val certFactory = CertificateFactory.getInstance("X.509")
+                return certFactory.generateCertificate(inputStream) as T
+            }
+        }
+    }
+
+    @Test
+    fun `metadata endpoint when url customized then used`() {
+        this.spring.register(Saml2LoginCustomEndpointConfig::class.java).autowire()
+		this.mockMvc.get("/saml/metadata")
+			.andExpect {
+				status { isOk() }
+			}
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    open class Saml2LoginCustomEndpointConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                saml2Metadata {
+                    metadataUrl = "/saml/metadata"
+                }
+            }
+            return http.build()
+        }
+
+        @Bean
+        open fun relyingPartyRegistrationRepository(): RelyingPartyRegistrationRepository? {
+			return InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build())
+        }
+    }
+
+	@Test
+	fun `metadata endpoint when resolver customized then used`() {
+		this.spring.register(Saml2LoginCustomMetadataResolverConfig::class.java).autowire()
+		val mocked = this.spring.context.getBean(Saml2MetadataResponseResolver::class.java)
+		every {
+			mocked.resolve(any())
+		} returns Saml2MetadataResponse("metadata", "file")
+		this.mockMvc.get("/saml2/metadata")
+			.andExpect {
+				status { isOk() }
+			}
+		verify(exactly = 1) { mocked.resolve(any()) }
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	open class Saml2LoginCustomMetadataResolverConfig {
+
+		private val metadataResponseResolver: Saml2MetadataResponseResolver = mockk()
+
+		@Bean
+		open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+			http {
+				saml2Metadata {}
+			}
+			return http.build()
+		}
+
+		@Bean
+		open fun metadataResponseResolver(): Saml2MetadataResponseResolver? {
+			return this.metadataResponseResolver
+		}
+
+		@Bean
+		open fun relyingPartyRegistrationRepository(): RelyingPartyRegistrationRepository? {
+			return InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build())
+		}
+	}
+}

+ 25 - 63
docs/modules/ROOT/pages/servlet/saml2/metadata.adoc

@@ -32,38 +32,25 @@ val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor();
 [[publishing-relying-party-metadata]]
 == Producing `<saml2:SPSSODescriptor>` Metadata
 
-You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
+You can publish a metadata endpoint using the `saml2Metadata` DSL method, as you'll see below:
 
 ====
 .Java
 [source,java,role="primary"]
 ----
-DefaultRelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
-        new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
-Saml2MetadataFilter filter = new Saml2MetadataFilter(
-        relyingPartyRegistrationResolver,
-        new OpenSamlMetadataResolver());
-
 http
     // ...
     .saml2Login(withDefaults())
-    .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
+    .saml2Metadata(withDefaults());
 ----
 
 .Kotlin
 [source,kotlin,role="secondary"]
 ----
-val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
-    DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
-val filter = Saml2MetadataFilter(
-    relyingPartyRegistrationResolver,
-    OpenSamlMetadataResolver()
-)
-
 http {
     //...
     saml2Login { }
-    addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
+    saml2Metadata { }
 }
 ----
 ====
@@ -71,77 +58,52 @@ http {
 You can use this metadata endpoint to register your relying party with your asserting party.
 This is often as simple as finding the correct form field to supply the metadata endpoint.
 
-By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
-You can change this by calling the `setRequestMatcher` method on the filter:
+By default, the metadata endpoint is `+/saml2/metadata+`, though it also responds to `+/saml2/metadata/{registrationId}+` and `+/saml2/service-provider-metadata/{registrationId}+`.
 
-====
-.Java
-[source,java,role="primary"]
-----
-filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
-----
-====
-
-Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
+You can change this by calling the `metadataUrl` method in the DSL:
 
 ====
 .Java
 [source,java,role="primary"]
 ----
-filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
+.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata"))
 ----
 
 .Kotlin
 [source,kotlin,role="secondary"]
 ----
-filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
+saml2Metadata {
+	metadataUrl = "/saml/metadata"
+}
 ----
 ====
 
 == Changing the Way a `RelyingPartyRegistration` Is Looked Up
 
-To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-RelyingPartyRegistrationResolver myRegistrationResolver = ...;
-Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver());
-
-// ...
-
-http.addFilterBefore(metadata, BasicAuthenticationFilter.class);
-----
-
-.Kotlin
-----
-val myRegistrationResolver: RelyingPartyRegistrationResolver = ...;
-val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver());
-
-// ...
-
-http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java);
-----
-====
-
-In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so:
+If you have a different strategy for identifying which `RelyingPartyRegistration` to use, you can configure your own `Saml2MetadataResponseResolver` like the one below:
 
 ====
 .Java
 [source,java,role="primary"]
 ----
-metadata.setRequestMatcher("/saml2/metadata")
+@Bean
+Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) {
+	RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(
+			(id) -> registrations.findByRegistrationId("relying-party"));
+	metadata.setMetadataFilename("metadata.xml");
+	return metadata;
+}
 ----
 
 .Kotlin
+[source,kotlin,role="secondary"]
 ----
-metadata.setRequestMatcher("/saml2/metadata")
+@Bean
+fun metadataResponseResolver(val registrations: RelyingPartyRegistrationRepository): Saml2MetadataResponseResolver {
+    val metadata = new RequestMatcherMetadataResponseResolver(
+			id: String -> registrations.findByRegistrationId("relying-party"))
+	metadata.setMetadataFilename("metadata.xml")
+	return metadata
+}
 ----
 ====