Make security auto-configs back off when SecurityFilterChain present

Closes gh-22739
This commit is contained in:
Madhura Bhave
2020-08-14 12:21:35 -07:00
parent c9b8a05321
commit bbbbe8e4d2
18 changed files with 264 additions and 90 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.security.oauth2.client.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -28,6 +29,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.web.SecurityFilterChain;
/**
* {@link WebSecurityConfigurerAdapter} to add OAuth client support.
@@ -52,7 +54,8 @@ class OAuth2WebSecurityConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnClass({ SecurityFilterChain.class, WebSecurityConfigurerAdapter.class })
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override

View File

@@ -21,6 +21,7 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
@@ -37,6 +38,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public
@@ -96,7 +98,8 @@ class OAuth2ResourceServerJwtConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnClass({ SecurityFilterChain.class, WebSecurityConfigurerAdapter.class })
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class OAuth2WebSecurityConfigurerAdapter {
@Bean

View File

@@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
@@ -26,6 +27,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;
/**
* Configures a {@link OpaqueTokenIntrospector} when a token introspection endpoint is
@@ -52,7 +54,8 @@ class OAuth2ResourceServerOpaqueTokenConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnClass({ SecurityFilterChain.class, WebSecurityConfigurerAdapter.class })
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class OAuth2WebSecurityConfigurerAdapter {
@Bean

View File

@@ -17,11 +17,13 @@
package org.springframework.boot.autoconfigure.security.saml2;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
/**
* {@link WebSecurityConfigurerAdapter} configuration for Spring Security's relying party
@@ -30,11 +32,12 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
* @author Madhura Bhave
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
@ConditionalOnBean(RelyingPartyRegistrationRepository.class)
@ConditionalOnClass({ SecurityFilterChain.class, WebSecurityConfigurerAdapter.class })
class Saml2LoginConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
static class Saml2LoginConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override

View File

@@ -24,22 +24,23 @@ import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.SecurityFilterChain;
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If the
* user specifies their own {@link WebSecurityConfigurerAdapter}, this will back-off
* completely and the users should specify all the bits that they want to configure as
* part of the custom security configuration.
* user specifies their own {@link WebSecurityConfigurerAdapter} or
* {@link SecurityFilterChain} bean, this will back-off completely and the users should
* specify all the bits that they want to configure as part of the custom security
* configuration.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnClass({ SecurityFilterChain.class, WebSecurityConfigurerAdapter.class })
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {
class SpringBootWebSecurityConfiguration {
@Configuration(proxyBeanMethods = false)
@Order(SecurityProperties.BASIC_AUTH_ORDER)

View File

@@ -16,31 +16,28 @@
package org.springframework.boot.autoconfigure.security.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* If there is a bean of type WebSecurityConfigurerAdapter, this adds the
* {@link EnableWebSecurity @EnableWebSecurity} annotation. This will make sure that the
* annotation is present with default security auto-configuration and also if the user
* adds custom security and forgets to add the annotation. If
* {@link EnableWebSecurity @EnableWebSecurity} has already been added or if a bean with
* name {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user,
* this will back-off.
* Adds the{@link EnableWebSecurity @EnableWebSecurity} annotation if Spring Security is
* on the classpath. This will make sure that the annotation is present with default
* security auto-configuration and also if the user adds custom security and forgets to
* add the annotation. If {@link EnableWebSecurity @EnableWebSecurity} has already been
* added or if a bean with name {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been
* configured by the user, this will back-off.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnClass(EnableWebSecurity.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
class WebSecurityEnablerConfiguration {
}

View File

@@ -24,6 +24,7 @@ import javax.servlet.Filter;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
@@ -31,6 +32,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
@@ -115,7 +117,7 @@ class OAuth2WebSecurityConfigurationTests {
}
@Test
void securityConfigurerBacksOffWhenOtherWebSecurityAdapterPresent() {
void securityConfigurerBacksOffBacksOffWhenOtherWebSecurityAdapterPresent() {
this.contextRunner
.withUserConfiguration(TestWebSecurityConfigurerConfig.class, OAuth2WebSecurityConfiguration.class)
.run((context) -> {
@@ -125,6 +127,28 @@ class OAuth2WebSecurityConfigurationTests {
});
}
@Test
void securityConfigurerBacksOffBacksOffWhenOtherSecurityFilterChainBeanPresent() {
this.contextRunner
.withUserConfiguration(TestSecurityFilterChainConfig.class, OAuth2WebSecurityConfiguration.class)
.run((context) -> {
assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty();
assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty();
assertThat(context).getBean(OAuth2AuthorizedClientService.class).isNotNull();
});
}
@Test
void securityConfigurerBacksOffConditionalOnSecurityFilterChainClass() {
this.contextRunner
.withUserConfiguration(ClientRegistrationRepositoryConfiguration.class,
OAuth2WebSecurityConfiguration.class)
.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> {
assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty();
assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty();
});
}
@Test
void authorizedClientServiceBeanIsConditionalOnMissingBean() {
this.contextRunner.withUserConfiguration(OAuth2AuthorizedClientServiceConfiguration.class,
@@ -211,6 +235,19 @@ class OAuth2WebSecurityConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@Import(ClientRegistrationRepositoryConfiguration.class)
static class TestSecurityFilterChainConfig {
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
.build();
}
}
@Configuration(proxyBeanMethods = false)
@Import(ClientRegistrationRepositoryConfiguration.class)
static class OAuth2AuthorizedClientServiceConfiguration {

View File

@@ -41,6 +41,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -279,6 +280,30 @@ class OAuth2ResourceServerAutoConfigurationTests {
});
}
@Test
void jwtSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> {
assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class);
assertThat(getBearerTokenFilter(context)).isNull();
});
}
@Test
void opaqueTokenSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> {
assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class);
assertThat(getBearerTokenFilter(context)).isNull();
});
}
@Test
void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() {
this.contextRunner
@@ -351,6 +376,24 @@ class OAuth2ResourceServerAutoConfigurationTests {
});
}
@Test
void jwtSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class, TestSecurityFilterChainConfig.class)
.run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class));
}
@Test
void opaqueTokenSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() {
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class)
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class));
}
private Filter getBearerTokenFilter(AssertableWebApplicationContext context) {
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
@@ -427,4 +470,16 @@ class OAuth2ResourceServerAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class TestSecurityFilterChainConfig {
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
.build();
}
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
@@ -47,7 +48,7 @@ import static org.mockito.Mockito.mock;
*
* @author Madhura Bhave
*/
public class Saml2RelyingPartyAutoConfigurationTests {
class Saml2RelyingPartyAutoConfigurationTests {
private static final String PREFIX = "spring.security.saml2.relyingparty.registration";
@@ -133,6 +134,20 @@ public class Saml2RelyingPartyAutoConfigurationTests {
.run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse());
}
@Test
void samlLoginShouldBackOffWhenASecurityFilterChainBeanIsPresent() {
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class)
.withPropertyValues(getPropertyValues())
.run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse());
}
@Test
void samlLoginShouldShouldBeConditionalOnSecurityWebFilterClass() {
this.contextRunner.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class))
.withPropertyValues(getPropertyValues())
.run((context) -> assertThat(context).doesNotHaveBean(SecurityFilterChain.class));
}
private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests) {
return new String[] { PREFIX
+ ".foo.identityprovider.singlesignon.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php",
@@ -183,4 +198,15 @@ public class Saml2RelyingPartyAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class TestSecurityFilterChainConfig {
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
.build();
}
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoCon
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.test.City;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.boot.web.servlet.filter.OrderedFilter;
@@ -37,12 +38,14 @@ import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,6 +70,28 @@ class SecurityAutoConfigurationTests {
});
}
@Test
void enableWebSecurityIsConditionalOnClass() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.security.config"))
.run((context) -> assertThat(context).doesNotHaveBean("springSecurityFilterChain"));
}
@Test
void securityConfigurerBacksOffWhenOtherSecurityFilterChainBeanPresent() {
this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> {
assertThat(context.getBeansOfType(SecurityFilterChain.class).size()).isEqualTo(1);
assertThat(context.containsBean("testSecurityFilterChain")).isTrue();
});
}
@Test
void securityConfigurerBacksOffWhenOtherWebSecurityAdapterBeanPresent() {
this.contextRunner.withUserConfiguration(WebSecurity.class).run((context) -> {
assertThat(context.getBeansOfType(WebSecurityConfigurerAdapter.class).size()).isEqualTo(1);
assertThat(context.containsBean("securityAutoConfigurationTests.WebSecurity")).isTrue();
});
}
@Test
void testDefaultFilterOrderWithSecurityAdapter() {
this.contextRunner
@@ -207,4 +232,16 @@ class SecurityAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class TestSecurityFilterChainConfig {
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
.build();
}
}
}