parent
785123eb2a
commit
46452c0cae
|
@ -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>
|
||||
* @EnableWebSecurity
|
||||
* @Configuration
|
||||
* public class Saml2LogoutSecurityConfig {
|
||||
* @Bean
|
||||
* public SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
* http
|
||||
* .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
* .saml2Metadata(Customizer.withDefaults());
|
||||
* return http.build();
|
||||
* }
|
||||
*
|
||||
* @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>
|
||||
* @EnableWebSecurity
|
||||
* @Configuration
|
||||
* public class Saml2LogoutSecurityConfig {
|
||||
* @Bean
|
||||
* public SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
* http
|
||||
* .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
* .saml2Metadata(Customizer.withDefaults());
|
||||
* return http.build();
|
||||
* }
|
||||
*
|
||||
* @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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
----
|
||||
====
|
||||
|
|
Loading…
Reference in New Issue