Add saml2Metadata

Closes gh-11828
This commit is contained in:
Josh Cummings 2023-03-09 09:23:45 -07:00
parent 785123eb2a
commit 46452c0cae
7 changed files with 780 additions and 64 deletions

View File

@ -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>

View File

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

View File

@ -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.
*

View File

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

View File

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

View File

@ -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())
}
}
}

View File

@ -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}+`.
You can change this by calling the `metadataUrl` method in the DSL:
====
.Java
[source,java,role="primary"]
----
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata"))
----
.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:
====
.Java
[source,java,role="primary"]
----
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
----
.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:
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"]
----
RelyingPartyRegistrationResolver myRegistrationResolver = ...;
Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver());
// ...
http.addFilterBefore(metadata, BasicAuthenticationFilter.class);
@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"]
----
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:
====
.Java
[source,java,role="primary"]
----
metadata.setRequestMatcher("/saml2/metadata")
----
.Kotlin
----
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
}
----
====