From 7f91d7ec5adc819ae0c758449cee7f900fb4b8ec Mon Sep 17 00:00:00 2001 From: "voytech.m" Date: Mon, 13 Dec 2021 18:07:41 +0100 Subject: [PATCH] gh-631 - Add support for customizing OAuth2FeignReqeustInterceptor.accessTokenProvider (injecting e.g. LoadBalancerInterceptor) (#642) --- docs/src/main/asciidoc/_configprops.adoc | 5 +- .../main/asciidoc/spring-cloud-openfeign.adoc | 10 ++ .../openfeign/FeignAutoConfiguration.java | 28 +++++- .../OAuth2FeignRequestInterceptorBuilder.java | 81 ++++++++++++++++ ...uth2FeignRequestInterceptorConfigurer.java | 35 +++++++ ...itional-spring-configuration-metadata.json | 12 +++ .../FeignAutoConfigurationTests.java | 80 ++++++++++++++++ ...viderWithLoadBalancerInterceptorTests.java | 95 +++++++++++++++++++ ...erWithoutLoadBalancerInterceptorTests.java | 95 +++++++++++++++++++ .../security/MockOAuth2ClientContext.java | 2 +- 10 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 3bba5138..31ff8861 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -30,5 +30,6 @@ |feign.httpclient.time-to-live-unit | | |feign.metrics.enabled | `true` | Enables metrics capability for Feign. |feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign. - -|=== \ No newline at end of file +|feign.oauth2.enabled | `false` | Enables the use OAuth2FeignRequestInterceptor to resolve access token. +|feign.oauth2.load-balanced | `false` | Enables `LoadBalancerInterceptor` or `RetryloadBalancerInterceptor` for `OAuth2FeignRequestInterceptor.accessTokenProvider` to use load balancing when resolving authentication token. +|=== diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 883fdee2..24885451 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -761,6 +761,16 @@ feign.client.refresh-enabled=true ---- TIP: DO NOT annotate the `@FeignClient` interface with the `@RefreshScope` annotation. +=== OAuth2 Support +OAuth2 support can be enabled by setting following flag: +---- +feign.oauth2.enabled=true +---- +When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2FeignRequestInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. +Sometimes, when load balancing is enabled for Feign clients, you may want to use load balancing for fetching access tokens, too. To do so, you should ensure that the load balancer is on the classpath (spring-cloud-starter-loadbalancer) and explicitly enable load balancing for OAuth2FeignRequestInterceptor by setting the following flag: +---- +feign.oauth2.load-balanced=true +---- == Configuration properties diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 56519ef0..cf55de2f 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -55,11 +55,14 @@ import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; +import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.cloud.openfeign.support.PageJacksonModule; @@ -73,6 +76,8 @@ import org.springframework.data.domain.Sort; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import static org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder.buildWithConfigurers; + /** * @author Spencer Gibb * @author Julien Roy @@ -84,6 +89,7 @@ import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResour * @author Andrii Bohutskyi * @author Kwangyong Kim * @author Sam Kruglov + * @author Wojciech Mąka */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @@ -322,12 +328,30 @@ public class FeignAutoConfiguration { @ConditionalOnProperty("feign.oauth2.enabled") protected static class Oauth2FeignConfiguration { + @ConditionalOnBean({ RetryLoadBalancerInterceptor.class, OAuth2ClientContext.class, + OAuth2ProtectedResourceDetails.class }) + @ConditionalOnProperty(value = "feign.oauth2.load-balanced", havingValue = "true") + @Bean + public OAuth2FeignRequestInterceptorConfigurer retryLoadBalancerInterceptorInjectingConfigurer( + final RetryLoadBalancerInterceptor loadBalancerInterceptor) { + return builder -> builder.withAccessTokenProviderInterceptors(loadBalancerInterceptor); + } + + @ConditionalOnBean({ LoadBalancerInterceptor.class, OAuth2ClientContext.class, + OAuth2ProtectedResourceDetails.class }) + @ConditionalOnProperty(value = "feign.oauth2.load-balanced", havingValue = "true") + @Bean + public OAuth2FeignRequestInterceptorConfigurer loadBalancerInterceptorInjectingConfigurer( + final LoadBalancerInterceptor loadBalancerInterceptor) { + return builder -> builder.withAccessTokenProviderInterceptors(loadBalancerInterceptor); + } + @Bean @ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class) @ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class }) public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource) { - return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource); + OAuth2ProtectedResourceDetails resource, List configurers) { + return buildWithConfigurers(oAuth2ClientContext, resource, configurers); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java new file mode 100644 index 00000000..62beba0b --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2021 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.cloud.openfeign.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.token.AccessTokenProvider; +import org.springframework.security.oauth2.client.token.AccessTokenProviderChain; +import org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; +import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider; +import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider; + +/** + * Allows to customize pre-defined {@link OAuth2FeignRequestInterceptor} using configurer + * beans of class {@link OAuth2FeignRequestInterceptorConfigurer}. Each configurer + * instance can add {@link AccessTokenProvider} new {@link ClientHttpRequestInterceptor} + * instances. + * + * @author Wojciech Mąka + * @since 3.1.1 + */ +public class OAuth2FeignRequestInterceptorBuilder { + + private AccessTokenProvider accessTokenProvider; + + private final List accessTokenProviderInterceptors = new ArrayList<>(); + + public OAuth2FeignRequestInterceptorBuilder() { + accessTokenProvider = new AccessTokenProviderChain(Arrays.asList( + new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(), + new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider())); + } + + public OAuth2FeignRequestInterceptorBuilder withAccessTokenProviderInterceptors( + ClientHttpRequestInterceptor... interceptors) { + accessTokenProviderInterceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + OAuth2FeignRequestInterceptor build(OAuth2ClientContext oAuth2ClientContext, + OAuth2ProtectedResourceDetails resource) { + if (OAuth2AccessTokenSupport.class.isAssignableFrom(accessTokenProvider.getClass())) { + ((OAuth2AccessTokenSupport) accessTokenProvider).setInterceptors(accessTokenProviderInterceptors); + } + final OAuth2FeignRequestInterceptor feignRequestInterceptor = new OAuth2FeignRequestInterceptor( + oAuth2ClientContext, resource); + feignRequestInterceptor.setAccessTokenProvider(accessTokenProvider); + return feignRequestInterceptor; + } + + public static OAuth2FeignRequestInterceptor buildWithConfigurers(OAuth2ClientContext oAuth2ClientContext, + OAuth2ProtectedResourceDetails resource, List buildConfigurers) { + final OAuth2FeignRequestInterceptorBuilder builder = new OAuth2FeignRequestInterceptorBuilder(); + for (OAuth2FeignRequestInterceptorConfigurer configurer : buildConfigurers) { + configurer.customize(builder); + } + return builder.build(oAuth2ClientContext, resource); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java new file mode 100644 index 00000000..a6442aa5 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-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.cloud.openfeign.security; + +import org.springframework.security.oauth2.client.token.AccessTokenProvider; + +/** + * Interface for configurer beans working with + * {@link OAuth2FeignRequestInterceptorBuilder} in order to provide custom interceptors + * for {@link AccessTokenProvider} managed internally by + * {@link OAuth2FeignRequestInterceptor}. + * + * @author Wojciech Mąka + * @since 3.1.1 + */ +@FunctionalInterface +public interface OAuth2FeignRequestInterceptorConfigurer { + + void customize(OAuth2FeignRequestInterceptorBuilder requestInterceptorBuilder); + +} diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 8b4fc57a..90a96468 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -61,6 +61,18 @@ "type": "java.lang.Boolean", "description": "Enables options value refresh capability for Feign.", "defaultValue": "false" + }, + { + "name": "feign.oauth2.enabled", + "type": "java.lang.Boolean", + "description": "Enables feign interceptor for managing oauth2 access token.", + "defaultValue": "false" + }, + { + "name": "feign.oauth2.load-balanced", + "type": "java.lang.Boolean", + "description": "Enables load balancing for oauth2 access token provider.", + "defaultValue": "false" } ] } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 9d9568ef..8fd1b482 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -23,10 +23,19 @@ import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver; +import org.springframework.cloud.openfeign.security.MockOAuth2ClientContext; +import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; +import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder; +import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -36,6 +45,7 @@ import static org.mockito.Mockito.mock; * @author Olga Maciaszek-Sharma * @author Andrii Bohutskyi * @author Kwangyong Kim + * @author Wojciech Mąka */ class FeignAutoConfigurationTests { @@ -81,6 +91,66 @@ class FeignAutoConfigurationTests { }); } + @Test + void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutInterceptors() { + runner.withPropertyValues("feign.oauth2.enabled=true").withBean(MockOAuth2ClientContext.class, "token") + .withBean(BaseOAuth2ProtectedResourceDetails.class) + .withBean(LoadBalancerInterceptor.class, () -> mock(LoadBalancerInterceptor.class)).run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorNotExists(ctx, LoadBalancerInterceptor.class); + }); + } + + @Test + void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithLoadBalancedInterceptor() { + runner.withPropertyValues("feign.oauth2.enabled=true", "feign.oauth2.load-balanced=true") + .withBean(MockOAuth2ClientContext.class, "token").withBean(BaseOAuth2ProtectedResourceDetails.class) + .withBean(LoadBalancerInterceptor.class, () -> mock(LoadBalancerInterceptor.class)).run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorExists(ctx, LoadBalancerInterceptor.class); + }); + } + + @Test + void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutLoadBalancedInterceptorIfNoBeanPresent() { + runner.withPropertyValues("feign.oauth2.enabled=true", "feign.oauth2.load-balanced=true") + .withBean(MockOAuth2ClientContext.class, "token").withBean(BaseOAuth2ProtectedResourceDetails.class) + .run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorNotExists(ctx, LoadBalancerInterceptor.class); + }); + } + + @Test + void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithCustomAccessTokenProviderInterceptor() { + runner.withPropertyValues("feign.oauth2.enabled=true").withBean(MockOAuth2ClientContext.class, "token") + .withBean(BaseOAuth2ProtectedResourceDetails.class) + .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); + }); + } + + private void assertOauth2FeignRequestInterceptorExists(ConfigurableApplicationContext ctx) { + AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); + assertThat(context).hasSingleBean(OAuth2FeignRequestInterceptor.class); + } + + private void assertAccessTokenProviderInterceptorExists(ConfigurableApplicationContext ctx, + Class clazz) { + AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + .extracting("interceptors").asList().first().isInstanceOf(clazz); + } + + private void assertAccessTokenProviderInterceptorNotExists(ConfigurableApplicationContext ctx, + Class clazz) { + AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + .extracting("interceptors").asList().filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) + .isEmpty(); + } + private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1).hasValueSatisfying(new Condition<>( beanClass::isInstance, String.format("Targeter should be an instance of %s", beanClass))); @@ -108,4 +178,14 @@ class FeignAutoConfigurationTests { } + static class CustomOAuth2FeignRequestInterceptorConfigurer implements OAuth2FeignRequestInterceptorConfigurer { + + @Override + public void customize(OAuth2FeignRequestInterceptorBuilder requestInterceptorBuilder) { + requestInterceptorBuilder + .withAccessTokenProviderInterceptors(new BasicAuthenticationInterceptor("username", "password")); + } + + } + } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java new file mode 100644 index 00000000..42cccac5 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-2021 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.cloud.openfeign.security; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Wojciech Mąka + */ +@SpringBootTest(classes = AccessTokenProviderWithLoadBalancerInterceptorTests.Application.class, + webEnvironment = RANDOM_PORT, + value = { "security.oauth2.client.id=test-service", "security.oauth2.client.client-id=test-service", + "security.oauth2.client.client-secret=test-service", + "security.oauth2.client.grant-type=client_credentials", "feign.oauth2.enabled=true", + "feign.oauth2.load-balanced=true" }) +@DirtiesContext +public class AccessTokenProviderWithLoadBalancerInterceptorTests { + + @Autowired + FeignContext context; + + @Autowired + private ConfigurableApplicationContext applicationContext; + + @Test + void testOAuth2RequestInterceptorIsLoadBalanced() { + AssertableApplicationContext assertableContext = AssertableApplicationContext.get(() -> applicationContext); + assertThat(assertableContext).hasSingleBean(Application.SampleClient.class); + assertThat(assertableContext).hasSingleBean(OAuth2FeignRequestInterceptor.class); + assertThat(assertableContext).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + .extracting("interceptors").asList() + .filteredOn(obj -> RetryLoadBalancerInterceptor.class.equals(obj.getClass())).hasSize(1); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @RestController + @EnableFeignClients( + clients = { AccessTokenProviderWithLoadBalancerInterceptorTests.Application.SampleClient.class }) + protected static class Application { + + @GetMapping("/foo") + public String foo(HttpServletRequest request) throws IllegalAccessException { + if ("Foo".equals(request.getHeader("Foo")) && "Bar".equals(request.getHeader("Bar"))) { + return "OK"; + } + else { + throw new IllegalAccessException("It should has Foo and Bar header"); + } + } + + @FeignClient(name = "sampleClient") + protected interface SampleClient { + + @GetMapping("/foo") + String foo(); + + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java new file mode 100644 index 00000000..1340e081 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-2021 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.cloud.openfeign.security; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Wojciech Mąka + */ +@SpringBootTest(classes = AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.class, + webEnvironment = RANDOM_PORT, + value = { "security.oauth2.client.id=test-service", "security.oauth2.client.client-id=test-service", + "security.oauth2.client.client-secret=test-service", + "security.oauth2.client.grant-type=client_credentials", "feign.oauth2.enabled=true" }) +@DirtiesContext +public class AccessTokenProviderWithoutLoadBalancerInterceptorTests { + + @Autowired + FeignContext context; + + @Autowired + private ConfigurableApplicationContext applicationContext; + + @Test + void testOAuth2RequestInterceptorIsNotLoadBalanced() { + AssertableApplicationContext assertableContext = AssertableApplicationContext.get(() -> applicationContext); + assertThat(assertableContext) + .hasSingleBean(AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.SampleClient.class); + assertThat(assertableContext).hasSingleBean(OAuth2FeignRequestInterceptor.class); + assertThat(assertableContext).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + .extracting("interceptors").asList() + .filteredOn(obj -> RetryLoadBalancerInterceptor.class.equals(obj.getClass())).isEmpty(); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @RestController + @EnableFeignClients( + clients = { AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.SampleClient.class }) + protected static class Application { + + @GetMapping("/foo") + public String foo(HttpServletRequest request) throws IllegalAccessException { + if ("Foo".equals(request.getHeader("Foo")) && "Bar".equals(request.getHeader("Bar"))) { + return "OK"; + } + else { + throw new IllegalAccessException("It should has Foo and Bar header"); + } + } + + @FeignClient(name = "sampleClient") + protected interface SampleClient { + + @GetMapping("/foo") + String foo(); + + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java index 237642a9..3aefecbd 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java @@ -29,7 +29,7 @@ import org.springframework.security.oauth2.common.OAuth2AccessToken; * * @author João Pedro Evangelista */ -final class MockOAuth2ClientContext implements OAuth2ClientContext { +public final class MockOAuth2ClientContext implements OAuth2ClientContext { private final String value;