Commit 342a0535 authored by Madhura Bhave's avatar Madhura Bhave

Explicitly configure SecurityWebFilterChain bean for reactive oauth2 client

This will ensure that ReactiveManagementWebSecurityAutoConfiguration backs
off and that the actuator endpoints are also secured via OAuth2.

Fixes gh-17949
parent c6134184
......@@ -15,34 +15,21 @@
*/
package org.springframework.boot.autoconfigure.security.oauth2.client.reactive;
import java.util.ArrayList;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security's Reactive
......@@ -56,39 +43,10 @@ import org.springframework.security.oauth2.client.web.server.ServerOAuth2Authori
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ReactiveOAuth2ClientAutoConfiguration.NonServletApplicationCondition.class)
@ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, ClientRegistration.class })
@Import({ ReactiveOAuth2ClientConfigurations.ReactiveClientRegistrationRepositoryConfiguration.class,
ReactiveOAuth2ClientConfigurations.ReactiveOAuth2ClientConfiguration.class })
public class ReactiveOAuth2ClientAutoConfiguration {
private final OAuth2ClientProperties properties;
public ReactiveOAuth2ClientAutoConfiguration(OAuth2ClientProperties properties) {
this.properties = properties;
}
@Bean
@Conditional(ClientsConfiguredCondition.class)
@ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class)
public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(this.properties).values());
return new InMemoryReactiveClientRegistrationRepository(registrations);
}
@Bean
@ConditionalOnBean(ReactiveClientRegistrationRepository.class)
@ConditionalOnMissingBean
public ReactiveOAuth2AuthorizedClientService authorizedClientService(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
@ConditionalOnBean(ReactiveOAuth2AuthorizedClientService.class)
@ConditionalOnMissingBean
public ServerOAuth2AuthorizedClientRepository authorizedClientRepository(
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
}
static class NonServletApplicationCondition extends NoneNestedConditions {
NonServletApplicationCondition() {
......
/*
* Copyright 2012-2019 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.boot.autoconfigure.security.oauth2.client.reactive;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* Reactive OAuth2 Client configurations.
*
* @author Madhura Bhave
*/
class ReactiveOAuth2ClientConfigurations {
@Configuration
@Conditional(ClientsConfiguredCondition.class)
@ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class)
static class ReactiveClientRegistrationRepositoryConfiguration {
@Bean
public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
return new InMemoryReactiveClientRegistrationRepository(registrations);
}
}
@Configuration
@ConditionalOnBean(ReactiveClientRegistrationRepository.class)
static class ReactiveOAuth2ClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactiveOAuth2AuthorizedClientService authorizedClientService(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
@ConditionalOnMissingBean
public ServerOAuth2AuthorizedClientRepository authorizedClientRepository(
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
static class SecurityWebFilterChainConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().anyExchange().authenticated();
http.oauth2Login();
return http.build();
}
}
}
}
......@@ -18,18 +18,27 @@ package org.springframework.boot.autoconfigure.security.oauth2.client.reactive;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import reactor.core.publisher.Flux;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
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.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
......@@ -38,7 +47,11 @@ import org.springframework.security.oauth2.client.registration.InMemoryReactiveC
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.WebFilter;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -49,8 +62,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class ReactiveOAuth2ClientAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientAutoConfiguration.class));
private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
.of(ReactiveOAuth2ClientAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class));
private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration";
......@@ -82,15 +95,19 @@ public class ReactiveOAuth2ClientAutoConfigurationTests {
}
@Test
public void authorizedClientServiceBeanIsConditionalOnClientRegistrationRepository() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOAuth2AuthorizedClientService.class));
public void authorizedClientServiceAndRepositoryBeansAreConditionalOnClientRegistrationRepository() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(ReactiveOAuth2AuthorizedClientService.class);
assertThat(context).doesNotHaveBean(ServerOAuth2AuthorizedClientRepository.class);
});
}
@Test
public void configurationRegistersAuthorizedClientServiceBean() {
this.contextRunner.withUserConfiguration(ReactiveClientRepositoryConfiguration.class).run(
(context) -> assertThat(context).hasSingleBean(InMemoryReactiveClientRegistrationRepository.class));
public void configurationRegistersAuthorizedClientServiceAndRepositoryBeans() {
this.contextRunner.withUserConfiguration(ReactiveClientRepositoryConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(InMemoryReactiveOAuth2AuthorizedClientService.class);
assertThat(context).hasSingleBean(AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository.class);
});
}
@Test
......@@ -124,6 +141,22 @@ public class ReactiveOAuth2ClientAutoConfigurationTests {
});
}
@Test
public void securityWebFilterChainBeanConditionalOnWebApplication() {
this.contextRunner.withUserConfiguration(ReactiveOAuth2AuthorizedClientRepositoryConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(SecurityWebFilterChain.class));
}
@Test
public void configurationRegistersSecurityWebFilterChainBean() { // gh-17949
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientAutoConfiguration.class))
.withUserConfiguration(ReactiveOAuth2AuthorizedClientServiceConfiguration.class,
ServerHttpSecurityConfiguration.class)
.run((context) -> assertThat(getFilters(context, OAuth2LoginAuthenticationWebFilter.class))
.isNotNull());
}
@Test
public void autoConfigurationConditionalOnClassFlux() {
assertWhenClassNotPresent(Flux.class);
......@@ -147,6 +180,15 @@ public class ReactiveOAuth2ClientAutoConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOAuth2ClientAutoConfiguration.class));
}
@SuppressWarnings("unchecked")
private List<WebFilter> getFilters(AssertableReactiveWebApplicationContext context,
Class<? extends WebFilter> filter) {
SecurityWebFilterChain filterChain = (SecurityWebFilterChain) context
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
List<WebFilter> filters = (List<WebFilter>) ReflectionTestUtils.getField(filterChain, "filters");
return filters.stream().filter(filter::isInstance).collect(Collectors.toList());
}
@Configuration
static class ReactiveClientRepositoryConfiguration {
......@@ -196,4 +238,24 @@ public class ReactiveOAuth2ClientAutoConfigurationTests {
}
@Configuration
static class ServerHttpSecurityConfiguration {
@Bean
ServerHttpSecurity http() {
TestServerHttpSecurity httpSecurity = new TestServerHttpSecurity();
return httpSecurity;
}
static class TestServerHttpSecurity extends ServerHttpSecurity implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
super.setApplicationContext(applicationContext);
}
}
}
}
......@@ -16,6 +16,10 @@
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
......
......@@ -51,4 +51,10 @@ public class SampleReactiveOAuth2ClientApplicationTests {
assertThat(bodyString).contains("/oauth2/authorization/github-client-2");
}
@Test
public void actuatorShouldBeSecuredByOAuth() {
this.webTestClient.get().uri("/actuator/health").exchange().expectStatus().isFound().expectHeader()
.valueEquals("Location", "/login");
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment