Skip to content

Commit 46452c0

Browse files
committed
Add saml2Metadata
Closes gh-11828
1 parent 785123e commit 46452c0

File tree

7 files changed

+779
-63
lines changed

7 files changed

+779
-63
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
7474
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
7575
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
76+
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer;
7677
import org.springframework.security.core.Authentication;
7778
import org.springframework.security.core.context.SecurityContext;
7879
import org.springframework.security.core.context.SecurityContextHolder;
@@ -2425,6 +2426,102 @@ public Saml2LogoutConfigurer<HttpSecurity> saml2Logout() throws Exception {
24252426
return getOrApply(new Saml2LogoutConfigurer<>(getContext()));
24262427
}
24272428

2429+
/**
2430+
* Configures a SAML 2.0 metadata endpoint that presents relying party configurations
2431+
* in an {@code <md:EntityDescriptor>} payload.
2432+
*
2433+
* <p>
2434+
* By default, the endpoints are {@code /saml2/metadata} and
2435+
* {@code /saml2/metadata/{registrationId}} though note that also
2436+
* {@code /saml2/service-provider-metadata/{registrationId}} is recognized for
2437+
* backward compatibility purposes.
2438+
*
2439+
* <p>
2440+
* <h2>Example Configuration</h2>
2441+
*
2442+
* The following example shows the minimal configuration required, using a
2443+
* hypothetical asserting party.
2444+
*
2445+
* <pre>
2446+
* &#064;EnableWebSecurity
2447+
* &#064;Configuration
2448+
* public class Saml2LogoutSecurityConfig {
2449+
* &#064;Bean
2450+
* public SecurityFilterChain web(HttpSecurity http) throws Exception {
2451+
* http
2452+
* .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
2453+
* .saml2Metadata(Customizer.withDefaults());
2454+
* return http.build();
2455+
* }
2456+
*
2457+
* &#064;Bean
2458+
* public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
2459+
* RelyingPartyRegistration registration = RelyingPartyRegistrations
2460+
* .withMetadataLocation("https://ap.example.org/metadata")
2461+
* .registrationId("simple")
2462+
* .build();
2463+
* return new InMemoryRelyingPartyRegistrationRepository(registration);
2464+
* }
2465+
* }
2466+
* </pre>
2467+
* @param saml2MetadataConfigurer the {@link Customizer} to provide more options for
2468+
* the {@link Saml2MetadataConfigurer}
2469+
* @return the {@link HttpSecurity} for further customizations
2470+
* @throws Exception
2471+
* @since 6.1
2472+
*/
2473+
public HttpSecurity saml2Metadata(Customizer<Saml2MetadataConfigurer<HttpSecurity>> saml2MetadataConfigurer)
2474+
throws Exception {
2475+
saml2MetadataConfigurer.customize(getOrApply(new Saml2MetadataConfigurer<>(getContext())));
2476+
return HttpSecurity.this;
2477+
}
2478+
2479+
/**
2480+
* Configures a SAML 2.0 metadata endpoint that presents relying party configurations
2481+
* in an {@code <md:EntityDescriptor>} payload.
2482+
*
2483+
* <p>
2484+
* By default, the endpoints are {@code /saml2/metadata} and
2485+
* {@code /saml2/metadata/{registrationId}} though note that also
2486+
* {@code /saml2/service-provider-metadata/{registrationId}} is recognized for
2487+
* backward compatibility purposes.
2488+
*
2489+
* <p>
2490+
* <h2>Example Configuration</h2>
2491+
*
2492+
* The following example shows the minimal configuration required, using a
2493+
* hypothetical asserting party.
2494+
*
2495+
* <pre>
2496+
* &#064;EnableWebSecurity
2497+
* &#064;Configuration
2498+
* public class Saml2LogoutSecurityConfig {
2499+
* &#064;Bean
2500+
* public SecurityFilterChain web(HttpSecurity http) throws Exception {
2501+
* http
2502+
* .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
2503+
* .saml2Metadata(Customizer.withDefaults());
2504+
* return http.build();
2505+
* }
2506+
*
2507+
* &#064;Bean
2508+
* public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
2509+
* RelyingPartyRegistration registration = RelyingPartyRegistrations
2510+
* .withMetadataLocation("https://ap.example.org/metadata")
2511+
* .registrationId("simple")
2512+
* .build();
2513+
* return new InMemoryRelyingPartyRegistrationRepository(registration);
2514+
* }
2515+
* }
2516+
* </pre>
2517+
* @return the {@link Saml2MetadataConfigurer} for further customizations
2518+
* @throws Exception
2519+
* @since 6.1
2520+
*/
2521+
public Saml2MetadataConfigurer<HttpSecurity> saml2Metadata() throws Exception {
2522+
return getOrApply(new Saml2MetadataConfigurer<>(getContext()));
2523+
}
2524+
24282525
/**
24292526
* Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0
24302527
* Provider. <br>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers.saml2;
18+
19+
import java.util.function.Function;
20+
21+
import org.springframework.context.ApplicationContext;
22+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
23+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
24+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
25+
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
26+
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
27+
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
28+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
29+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
30+
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
31+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
32+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
33+
import org.springframework.util.Assert;
34+
35+
/**
36+
* An {@link AbstractHttpConfigurer} for SAML 2.0 Metadata.
37+
*
38+
* <p>
39+
* SAML 2.0 Metadata provides an application with the capability to publish configuration
40+
* information as a {@code <md:EntityDescriptor>} or {@code <md:EntitiesDescriptor>}.
41+
*
42+
* <p>
43+
* Defaults are provided for all configuration options with the only required
44+
* configuration being a {@link Saml2LoginConfigurer#relyingPartyRegistrationRepository}.
45+
* Alternatively, a {@link RelyingPartyRegistrationRepository} {@code @Bean} may be
46+
* registered instead.
47+
*
48+
* <h2>Security Filters</h2>
49+
*
50+
* The following {@code Filter} is populated:
51+
*
52+
* <ul>
53+
* <li>{@link Saml2MetadataFilter}</li>
54+
* </ul>
55+
*
56+
* <h2>Shared Objects Created</h2>
57+
*
58+
* none
59+
*
60+
* <h2>Shared Objects Used</h2>
61+
*
62+
* The following shared objects are used:
63+
*
64+
* <ul>
65+
* <li>{@link RelyingPartyRegistrationRepository} (required)</li>
66+
* </ul>
67+
*
68+
* @since 6.1
69+
* @see HttpSecurity#saml2Metadata()
70+
* @see Saml2MetadataFilter
71+
* @see RelyingPartyRegistrationRepository
72+
*/
73+
public class Saml2MetadataConfigurer<H extends HttpSecurityBuilder<H>>
74+
extends AbstractHttpConfigurer<Saml2LogoutConfigurer<H>, H> {
75+
76+
private final ApplicationContext context;
77+
78+
private Function<RelyingPartyRegistrationRepository, Saml2MetadataResponseResolver> metadataResponseResolver;
79+
80+
public Saml2MetadataConfigurer(ApplicationContext context) {
81+
this.context = context;
82+
}
83+
84+
/**
85+
* Use this endpoint to request relying party metadata.
86+
*
87+
* <p>
88+
* If you specify a {@code registrationId} placeholder in the URL, then the filter
89+
* will lookup a {@link RelyingPartyRegistration} using that.
90+
*
91+
* <p>
92+
* If there is no {@code registrationId} and your
93+
* {@link RelyingPartyRegistrationRepository} is {code Iterable}, the metadata
94+
* endpoint will try and show all relying parties' metadata in a single
95+
* {@code <md:EntitiesDecriptor} element.
96+
*
97+
* <p>
98+
* If you need a more sophisticated lookup strategy than these, use
99+
* {@link #metadataResponseResolver} instead.
100+
* @param metadataUrl the url to use
101+
* @return the {@link Saml2MetadataConfigurer} for more customizations
102+
*/
103+
public Saml2MetadataConfigurer<H> metadataUrl(String metadataUrl) {
104+
Assert.hasText(metadataUrl, "metadataUrl cannot be empty");
105+
this.metadataResponseResolver = (registrations) -> {
106+
RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations,
107+
new OpenSamlMetadataResolver());
108+
metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl));
109+
return metadata;
110+
};
111+
return this;
112+
}
113+
114+
/**
115+
* Use this {@link Saml2MetadataResponseResolver} to parse the request and respond
116+
* with SAML 2.0 metadata.
117+
* @param metadataResponseResolver to use
118+
* @return the {@link Saml2MetadataConfigurer} for more customizations
119+
*/
120+
public Saml2MetadataConfigurer<H> metadataResponseResolver(Saml2MetadataResponseResolver metadataResponseResolver) {
121+
Assert.notNull(metadataResponseResolver, "metadataResponseResolver cannot be null");
122+
this.metadataResponseResolver = (registrations) -> metadataResponseResolver;
123+
return this;
124+
}
125+
126+
public H and() {
127+
return getBuilder();
128+
}
129+
130+
@Override
131+
public void configure(H http) throws Exception {
132+
Saml2MetadataResponseResolver metadataResponseResolver = createMetadataResponseResolver(http);
133+
http.addFilterBefore(new Saml2MetadataFilter(metadataResponseResolver), BasicAuthenticationFilter.class);
134+
}
135+
136+
private Saml2MetadataResponseResolver createMetadataResponseResolver(H http) {
137+
if (this.metadataResponseResolver != null) {
138+
RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
139+
return this.metadataResponseResolver.apply(registrations);
140+
}
141+
Saml2MetadataResponseResolver metadataResponseResolver = getBeanOrNull(Saml2MetadataResponseResolver.class);
142+
if (metadataResponseResolver != null) {
143+
return metadataResponseResolver;
144+
}
145+
RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
146+
return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver());
147+
}
148+
149+
private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {
150+
Saml2LoginConfigurer<H> login = http.getConfigurer(Saml2LoginConfigurer.class);
151+
if (login != null) {
152+
return login.relyingPartyRegistrationRepository(http);
153+
}
154+
else {
155+
return getBeanOrNull(RelyingPartyRegistrationRepository.class);
156+
}
157+
}
158+
159+
private <C> C getBeanOrNull(Class<C> clazz) {
160+
if (this.context == null) {
161+
return null;
162+
}
163+
if (this.context.getBeanNamesForType(clazz).length == 0) {
164+
return null;
165+
}
166+
return this.context.getBean(clazz);
167+
}
168+
169+
}

config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,43 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
677677
this.http.saml2Login(saml2LoginCustomizer)
678678
}
679679

680+
/**
681+
* Configures a SAML 2.0 relying party metadata endpoint.
682+
*
683+
* A [RelyingPartyRegistrationRepository] is required and must be registered with
684+
* the [ApplicationContext] or configured via
685+
* [Saml2Dsl.relyingPartyRegistrationRepository]
686+
*
687+
* Example:
688+
*
689+
* The following example shows the minimal configuration required, using a
690+
* hypothetical asserting party.
691+
*
692+
* ```
693+
* @Configuration
694+
* @EnableWebSecurity
695+
* class SecurityConfig {
696+
*
697+
* @Bean
698+
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
699+
* http {
700+
* saml2Login { }
701+
* saml2Metadata { }
702+
* }
703+
* return http.build()
704+
* }
705+
* }
706+
* ```
707+
* @param saml2MetadataConfiguration custom configuration to configure the
708+
* SAML2 relying party metadata endpoint
709+
* @see [Saml2MetadataDsl]
710+
* @since 6.1
711+
*/
712+
fun saml2Metadata(saml2MetadataConfiguration: Saml2MetadataDsl.() -> Unit) {
713+
val saml2MetadataCustomizer = Saml2MetadataDsl().apply(saml2MetadataConfiguration).get()
714+
this.http.saml2Metadata(saml2MetadataCustomizer)
715+
}
716+
680717
/**
681718
* Allows configuring how an anonymous user is represented.
682719
*
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web
18+
19+
import org.springframework.security.authentication.AuthenticationManagerResolver
20+
import org.springframework.security.config.annotation.web.builders.HttpSecurity
21+
import org.springframework.security.config.annotation.web.oauth2.resourceserver.JwtDsl
22+
import org.springframework.security.config.annotation.web.oauth2.resourceserver.OpaqueTokenDsl
23+
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
24+
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
25+
import org.springframework.security.web.AuthenticationEntryPoint
26+
import org.springframework.security.web.access.AccessDeniedHandler
27+
import jakarta.servlet.http.HttpServletRequest
28+
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer
29+
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver
30+
31+
/**
32+
* A Kotlin DSL to configure [HttpSecurity] SAML 2.0 relying party metadata support using
33+
* idiomatic Kotlin code.
34+
*
35+
* @author Josh Cummings
36+
* @since 6.1
37+
* @property metadataUrl the name of the relying party metadata endpoint; defaults to `/saml2/metadata` and `/saml2/metadata/{registrationId}`
38+
* @property metadataResponseResolver the [Saml2MetadataResponseResolver] to use for resolving the
39+
* metadata request into metadata
40+
*/
41+
@SecurityMarker
42+
class Saml2MetadataDsl {
43+
var metadataUrl: String? = null
44+
var metadataResponseResolver: Saml2MetadataResponseResolver? = null
45+
46+
internal fun get(): (Saml2MetadataConfigurer<HttpSecurity>) -> Unit {
47+
return { saml2Metadata ->
48+
metadataResponseResolver?.also { saml2Metadata.metadataResponseResolver(metadataResponseResolver) }
49+
metadataUrl?.also { saml2Metadata.metadataUrl(metadataUrl) }
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)