From 66cc168a332d40b57c4499715217b040e08e043f Mon Sep 17 00:00:00 2001 From: dzcr <1137729123@qq.com> Date: Fri, 23 Sep 2022 17:28:16 +0800 Subject: [PATCH 1/4] step 1 --- spring-cloud-openfeign-core/pom.xml | 4 + .../openfeign/FeignAutoConfiguration.java | 49 ++--- .../OAuth2AccessTokenInterceptor.java | 175 ++++++++++++++++++ .../FeignAutoConfigurationTests.java | 91 +++------ ...viderWithLoadBalancerInterceptorTests.java | 12 +- ...erWithoutLoadBalancerInterceptorTests.java | 10 +- .../OAuth2AccessTokenInterceptorTests.java | 167 +++++++++++++++++ spring-cloud-openfeign-dependencies/pom.xml | 6 + 8 files changed, 401 insertions(+), 113 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 6ac76c20..9045b45c 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -215,6 +215,10 @@ 2.11.0 test + + org.springframework.security + spring-security-oauth2-client + 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 c40bfb66..889aa3ce 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 @@ -29,7 +29,6 @@ import com.fasterxml.jackson.databind.Module; import feign.Capability; import feign.Client; import feign.Feign; -import feign.RequestInterceptor; import feign.Target; import feign.hc5.ApacheHttp5Client; import feign.httpclient.ApacheHttpClient; @@ -50,19 +49,17 @@ 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.client.OAuth2ClientProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.security.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.cloud.openfeign.support.PageJacksonModule; @@ -73,10 +70,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; 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; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; /** * @author Spencer Gibb @@ -90,6 +85,7 @@ import static org.springframework.cloud.openfeign.security.OAuth2FeignRequestInt * @author Kwangyong Kim * @author Sam Kruglov * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @@ -344,35 +340,20 @@ public class FeignAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(OAuth2ClientContext.class) + @EnableConfigurationProperties(OAuth2ClientProperties.class) @ConditionalOnProperty("spring.cloud.openfeign.oauth2.enabled") - @Deprecated // spring-security-oauth2 reached EOL protected static class Oauth2FeignConfiguration { - @ConditionalOnBean({ RetryLoadBalancerInterceptor.class, OAuth2ClientContext.class, - OAuth2ProtectedResourceDetails.class }) - @ConditionalOnProperty(value = "spring.cloud.openfeign.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 = "spring.cloud.openfeign.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, List configurers) { - return buildWithConfigurers(oAuth2ClientContext, resource, configurers); + @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) + public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( + @Value("${spring.cloud.openfeign.oauth2.specifiedClientIds:}") List specifiedClientIds, + OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + + return new OAuth2AccessTokenInterceptor(specifiedClientIds, oAuth2ClientProperties, + oAuth2AuthorizedClientService, clientRegistrationRepository); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java new file mode 100644 index 00000000..d695d871 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -0,0 +1,175 @@ +/* + * Copyright 2015-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.cloud.openfeign.security; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import feign.RequestInterceptor; +import feign.RequestTemplate; + +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.StringUtils; + +/** + * RequestInterceptor for OAuth2 Feign Requests. By default, It uses the + * {@link AuthorizedClientServiceOAuth2AuthorizedClientManager } to get + * {@link OAuth2AuthorizedClient } that hold an {@link OAuth2AccessToken }. Use the + * Client(s) from properties if not specific the field + * {@link OAuth2AccessTokenInterceptor#specifiedClientIds} + * + * @author Dangzhicairang(小水牛) + * @since 4.0.0 + */ +public class OAuth2AccessTokenInterceptor implements RequestInterceptor { + + /** + * The name of the token. + */ + public static final String BEARER = "Bearer"; + + /** + * The name of the header. + */ + public static final String AUTHORIZATION = "Authorization"; + + private final String tokenType; + + private final String header; + + private final List specifiedClientIds; + + private final OAuth2ClientProperties oAuth2ClientProperties; + + private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + + private OAuth2AuthorizedClientManager authorizedClientManager; + + public void setAuthorizedClientManager(OAuth2AuthorizedClientManager authorizedClientManager) { + this.authorizedClientManager = authorizedClientManager; + } + + private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", + "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + public OAuth2AccessTokenInterceptor(OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this(new ArrayList<>(), oAuth2ClientProperties, oAuth2AuthorizedClientService, clientRegistrationRepository); + } + + public OAuth2AccessTokenInterceptor(List specifiedClientIds, OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this(BEARER, AUTHORIZATION, specifiedClientIds, oAuth2ClientProperties, oAuth2AuthorizedClientService, + clientRegistrationRepository); + } + + public OAuth2AccessTokenInterceptor(String tokenType, String header, List specifiedClientIds, + OAuth2ClientProperties oAuth2ClientProperties, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this.tokenType = tokenType; + this.header = header; + this.specifiedClientIds = specifiedClientIds; + this.oAuth2ClientProperties = oAuth2ClientProperties; + this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; + this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, this.oAuth2AuthorizedClientService); + } + + @Override + public void apply(RequestTemplate template) { + template.header(header); + template.header(header, extract(tokenType)); + } + + protected String extract(String tokenType) { + OAuth2AccessToken accessToken = getToken(); + return String.format("%s %s", tokenType, accessToken.getTokenValue()); + } + + public OAuth2AccessToken getToken() { + + // if specific, try to use them to get token. + for (String clientId : this.specifiedClientIds) { + OAuth2AccessToken token = this.getToken(clientId); + if (token != null) { + return token; + } + } + + // use clients from properties by default + for (String clientId : Optional.ofNullable(this.oAuth2ClientProperties) + .map(OAuth2ClientProperties::getRegistration).map(Map::keySet).orElse(new HashSet<>())) { + OAuth2AccessToken token = this.getToken(clientId); + if (token != null) { + return token; + } + } + + throw new IllegalStateException("No token acquired, which is illegal according to the contract."); + } + + protected OAuth2AccessToken getToken(String clientId) { + + if (!StringUtils.hasText(clientId)) { + return null; + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + if (principal == null) { + principal = ANONYMOUS_AUTHENTICATION; + } + + // already exist + OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId, + principal.getName()); + if (oAuth2AuthorizedClient != null) { + OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken(); + if (accessToken != null && this.noExpire(accessToken)) { + return accessToken; + } + } + + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientId) + .principal(principal).build(); + OAuth2AuthorizedClient authorize = this.authorizedClientManager.authorize(authorizeRequest); + return Optional.ofNullable(authorize).map(OAuth2AuthorizedClient::getAccessToken).filter(this::noExpire) + .orElse(null); + } + + protected boolean noExpire(OAuth2AccessToken token) { + return Optional.ofNullable(token).map(OAuth2AccessToken::getExpiresAt) + .map(expire -> expire.isAfter(Instant.now())).orElse(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 bcf67e35..222fa3f3 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 @@ -17,6 +17,8 @@ package org.springframework.cloud.openfeign; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import feign.Target; import org.assertj.core.api.Condition; @@ -26,16 +28,11 @@ 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.AlphanumericCircuitBreakerNameResolver; -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.cloud.openfeign.security.OAuth2AccessTokenInterceptor; 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 org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -46,6 +43,7 @@ import static org.mockito.Mockito.mock; * @author Andrii Bohutskyi * @author Kwangyong Kim * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ class FeignAutoConfigurationTests { @@ -93,63 +91,30 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutInterceptors() { - runner.withPropertyValues("spring.cloud.openfeign.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("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.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("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.load-balanced=true").withBean(MockOAuth2ClientContext.class, "token") - .withBean(BaseOAuth2ProtectedResourceDetails.class).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorNotExists(ctx, LoadBalancerInterceptor.class); + "spring.cloud.openfeign.oauth2.specifiedClientIds=feign-client") + .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) + .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) + .run(ctx -> { + assertOauth2AccessTokenInterceptorExists(ctx); + assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, + new ArrayList() { + { + add("feign-client"); + } + }); }); } - @Test - void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithCustomAccessTokenProviderInterceptor() { - runner.withPropertyValues("spring.cloud.openfeign.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) { + private void assertOauth2AccessTokenInterceptorExists(ConfigurableApplicationContext ctx) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") - .extracting("interceptors").asList().first().isInstanceOf(clazz); + assertThat(context).hasSingleBean(OAuth2AccessTokenInterceptor.class); } - 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 assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue( + ConfigurableApplicationContext ctx, List expectedValue) { + final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); + assertThat(bean).hasFieldOrPropertyWithValue("specifiedClientIds", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { @@ -179,14 +144,4 @@ 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 index cec77e98..767ab515 100644 --- 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 @@ -41,11 +41,11 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen * @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", "spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.load-balanced=true" }) + 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", "spring.cloud.openfeign.oauth2.enabled=true", + "spring.cloud.openfeign.oauth2.load-balanced=true" }) @DirtiesContext public class AccessTokenProviderWithLoadBalancerInterceptorTests { @@ -70,7 +70,7 @@ public class AccessTokenProviderWithLoadBalancerInterceptorTests { @EnableAutoConfiguration @RestController @EnableFeignClients( - clients = { AccessTokenProviderWithLoadBalancerInterceptorTests.Application.SampleClient.class }) + clients = { AccessTokenProviderWithLoadBalancerInterceptorTests.Application.SampleClient.class }) protected static class Application { @GetMapping("/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 index 4693a137..d6c55f82 100644 --- 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 @@ -41,10 +41,10 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen * @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", "spring.cloud.openfeign.oauth2.enabled=true" }) + 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", "spring.cloud.openfeign.oauth2.enabled=true" }) @DirtiesContext public class AccessTokenProviderWithoutLoadBalancerInterceptorTests { @@ -70,7 +70,7 @@ public class AccessTokenProviderWithoutLoadBalancerInterceptorTests { @EnableAutoConfiguration @RestController @EnableFeignClients( - clients = { AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.SampleClient.class }) + clients = { AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.SampleClient.class }) protected static class Application { @GetMapping("/foo") diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java new file mode 100644 index 00000000..3066bd61 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2015-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.cloud.openfeign.security; + +import java.time.Instant; +import java.util.HashMap; + +import feign.Request.HttpMethod; +import feign.RequestTemplate; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.AlternativeJdkIdGenerator; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Dangzhicairang(小水牛) + */ +class OAuth2AccessTokenInterceptorTests { + + private OAuth2AccessTokenInterceptor oAuth2AccessTokenInterceptor; + + private RequestTemplate requestTemplate; + + private OAuth2ClientProperties mockOAuth2ClientProperties; + + private static final String DEFAULT_CLIENT_ID = "feign-client"; + + @BeforeEach + void setUp() { + + requestTemplate = new RequestTemplate().method(HttpMethod.GET); + + mockOAuth2ClientProperties = mock(OAuth2ClientProperties.class); + given(mockOAuth2ClientProperties.getRegistration()) + .willReturn(new HashMap() { + { + put(DEFAULT_CLIENT_ID, mock(OAuth2ClientProperties.Registration.class)); + } + }); + + } + + @Test + void noTokenAcquired() { + + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(null); + + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + Assertions.assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("No token acquired, which is illegal according to the contract."); + + } + + @Test + void validTokenAcquired() { + + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + Assertions.assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void expireTokenAcquired() { + + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(expiredTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + Assertions.assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("No token acquired, which is illegal according to the contract."); + } + + @Test + void acquireTokenFromAuthorizedClient() { + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())) + .willReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + Assertions.assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + private OAuth2AccessToken validToken() { + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Valid Token", Instant.now(), + Instant.now().plusSeconds(60L)); + } + + private OAuth2AccessToken expiredToken() { + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Expired Token", + Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L)); + } + + private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() { + return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", validToken()); + } + + private OAuth2AuthorizedClient expiredTokenOAuth2AuthorizedClient() { + return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", expiredToken()); + } + + private ClientRegistration defaultClientRegistration() { + return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId().toString()) + .clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + } + +} diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index 9c10c686..98cd1aa5 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -19,6 +19,7 @@ 3.8.0 2.5.2 + 6.0.0-SNAPSHOT @@ -27,6 +28,11 @@ spring-security-oauth2-autoconfigure ${spring-security-oauth2-autoconfigure.version} + + org.springframework.security + spring-security-oauth2-client + ${spring-security-oauth2-client.version} + org.springframework.cloud spring-cloud-openfeign-core From 8d3d5c574fe4e86715c302aa4883d41f651f876c Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 27 Sep 2022 15:14:54 +0200 Subject: [PATCH 2/4] Refactor. Add docs. Fix dependencies. --- .../main/asciidoc/spring-cloud-openfeign.adoc | 9 +- spring-cloud-openfeign-core/pom.xml | 20 +- .../openfeign/FeignAutoConfiguration.java | 12 +- .../OAuth2AccessTokenInterceptor.java | 98 +++++----- .../OAuth2FeignRequestInterceptor.java | 183 ------------------ .../OAuth2FeignRequestInterceptorBuilder.java | 81 -------- ...uth2FeignRequestInterceptorConfigurer.java | 35 ---- ...itional-spring-configuration-metadata.json | 8 +- .../FeignAutoConfigurationTests.java | 15 +- ...viderWithLoadBalancerInterceptorTests.java | 96 --------- ...erWithoutLoadBalancerInterceptorTests.java | 96 --------- .../security/MockAccessTokenProvider.java | 65 ------- .../security/MockOAuth2AccessToken.java | 79 -------- .../security/MockOAuth2ClientContext.java | 67 ------- .../OAuth2AccessTokenInterceptorTests.java | 121 ++++++------ .../OAuth2FeignRequestInterceptorTests.java | 118 ----------- spring-cloud-openfeign-dependencies/pom.xml | 13 -- 17 files changed, 125 insertions(+), 991 deletions(-) delete mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java delete mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java delete mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java delete mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockAccessTokenProvider.java delete mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2AccessToken.java delete mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java delete mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 994476dd..bebace63 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -809,11 +809,10 @@ OAuth2 support can be enabled by setting following flag: ---- spring.cloud.openfeign.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: ----- -spring.cloud.openfeign.oauth2.load-balanced=true ----- +When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. +`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientId` using the `spring.cloud.openfeign.oauth2.clientId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. + +TIP:: Using the `serviceId` as OAuth2 client id is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientId` is a suitable approach. === Transform the load-balanced HTTP request diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 9045b45c..64174855 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -132,25 +132,6 @@ okhttp true - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - true - - - javax.xml.bind - jaxb-api - - - javax.activation - javax.activation-api - - - com.sun.activation - jakarta.activation - - - org.springframework.boot spring-boot-autoconfigure-processor @@ -218,6 +199,7 @@ org.springframework.security spring-security-oauth2-client + true 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 889aa3ce..653e5157 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 @@ -49,7 +49,6 @@ 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.client.OAuth2ClientProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.cloud.client.actuator.HasFeatures; @@ -70,6 +69,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -340,20 +340,18 @@ public class FeignAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(OAuth2ClientProperties.class) + @ConditionalOnClass(OAuth2AuthorizedClientManager.class) @ConditionalOnProperty("spring.cloud.openfeign.oauth2.enabled") protected static class Oauth2FeignConfiguration { @Bean @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( - @Value("${spring.cloud.openfeign.oauth2.specifiedClientIds:}") List specifiedClientIds, - OAuth2ClientProperties oAuth2ClientProperties, + @Value("${spring.cloud.openfeign.oauth2.clientId:}") String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - - return new OAuth2AccessTokenInterceptor(specifiedClientIds, oAuth2ClientProperties, - oAuth2AuthorizedClientService, clientRegistrationRepository); + return new OAuth2AccessTokenInterceptor(clientId, oAuth2AuthorizedClientService, + clientRegistrationRepository); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index d695d871..b421d0ae 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -16,17 +16,14 @@ package org.springframework.cloud.openfeign.security; +import java.net.URI; import java.time.Instant; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; import java.util.Optional; import feign.RequestInterceptor; import feign.RequestTemplate; +import feign.Target; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -38,16 +35,22 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * RequestInterceptor for OAuth2 Feign Requests. By default, It uses the + * A {@link RequestInterceptor} for OAuth2 Feign Requests. By default, it uses the * {@link AuthorizedClientServiceOAuth2AuthorizedClientManager } to get - * {@link OAuth2AuthorizedClient } that hold an {@link OAuth2AccessToken }. Use the - * Client(s) from properties if not specific the field - * {@link OAuth2AccessTokenInterceptor#specifiedClientIds} + * {@link OAuth2AuthorizedClient } that holds an {@link OAuth2AccessToken }. If the user + * has specified an OAuth2 {@code clientId} using the + * {@code spring.cloud.openfeign.oauth2.clientId} property, it will be used to retrieve + * the token. If the token is not retrieved or the {@code clientId} has not been + * specified, the {@code serviceId} retrieved from the {@code url} host segment will be + * used. This approach is convenient for load-balanced Feign clients. For + * non-load-balanced ones, the property-based {@code clientId} is a suitable approach. * * @author Dangzhicairang(小水牛) + * @author Olga Maciaszek-Sharma * @since 4.0.0 */ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { @@ -66,9 +69,7 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private final String header; - private final List specifiedClientIds; - - private final OAuth2ClientProperties oAuth2ClientProperties; + private final String clientId; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; @@ -81,26 +82,22 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - public OAuth2AccessTokenInterceptor(OAuth2ClientProperties oAuth2ClientProperties, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - this(new ArrayList<>(), oAuth2ClientProperties, oAuth2AuthorizedClientService, clientRegistrationRepository); + this(null, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(List specifiedClientIds, OAuth2ClientProperties oAuth2ClientProperties, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + public OAuth2AccessTokenInterceptor(String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - this(BEARER, AUTHORIZATION, specifiedClientIds, oAuth2ClientProperties, oAuth2AuthorizedClientService, - clientRegistrationRepository); + this(BEARER, AUTHORIZATION, clientId, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(String tokenType, String header, List specifiedClientIds, - OAuth2ClientProperties oAuth2ClientProperties, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientId, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { this.tokenType = tokenType; this.header = header; - this.specifiedClientIds = specifiedClientIds; - this.oAuth2ClientProperties = oAuth2ClientProperties; + this.clientId = clientId; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( clientRegistrationRepository, this.oAuth2AuthorizedClientService); @@ -108,39 +105,31 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { + OAuth2AccessToken token = getToken(template); + String extractedToken = String.format("%s %s", tokenType, token.getTokenValue()); template.header(header); - template.header(header, extract(tokenType)); + template.header(header, extractedToken); } - protected String extract(String tokenType) { - OAuth2AccessToken accessToken = getToken(); - return String.format("%s %s", tokenType, accessToken.getTokenValue()); - } - - public OAuth2AccessToken getToken() { - - // if specific, try to use them to get token. - for (String clientId : this.specifiedClientIds) { - OAuth2AccessToken token = this.getToken(clientId); + public OAuth2AccessToken getToken(RequestTemplate template) { + // If specified, try to use them to get token. + if (StringUtils.hasText(clientId)) { + OAuth2AccessToken token = getToken(clientId); if (token != null) { return token; } } - // use clients from properties by default - for (String clientId : Optional.ofNullable(this.oAuth2ClientProperties) - .map(OAuth2ClientProperties::getRegistration).map(Map::keySet).orElse(new HashSet<>())) { - OAuth2AccessToken token = this.getToken(clientId); - if (token != null) { - return token; - } + // If not specified use host (synonymous with serviceId for load-balanced + // requests; non-load-balanced requests should use the method above). + OAuth2AccessToken token = getToken(getServiceId(template)); + if (token != null) { + return token; } - - throw new IllegalStateException("No token acquired, which is illegal according to the contract."); + throw new IllegalStateException("OAuth2 token has not been successfully acquired."); } protected OAuth2AccessToken getToken(String clientId) { - if (!StringUtils.hasText(clientId)) { return null; } @@ -150,26 +139,35 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { principal = ANONYMOUS_AUTHENTICATION; } - // already exist + // Already exist OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId, principal.getName()); if (oAuth2AuthorizedClient != null) { OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken(); - if (accessToken != null && this.noExpire(accessToken)) { + if (accessToken != null && notExpired(accessToken)) { return accessToken; } } OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientId) .principal(principal).build(); - OAuth2AuthorizedClient authorize = this.authorizedClientManager.authorize(authorizeRequest); - return Optional.ofNullable(authorize).map(OAuth2AuthorizedClient::getAccessToken).filter(this::noExpire) - .orElse(null); + OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); + return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken) + .filter(this::notExpired).orElse(null); } - protected boolean noExpire(OAuth2AccessToken token) { + protected boolean notExpired(OAuth2AccessToken token) { return Optional.ofNullable(token).map(OAuth2AccessToken::getExpiresAt) .map(expire -> expire.isAfter(Instant.now())).orElse(false); } + private static String getServiceId(RequestTemplate template) { + Target feignTarget = template.feignTarget(); + Assert.notNull(feignTarget, "feignTarget may not be null"); + String url = feignTarget.url(); + Assert.hasLength(url, "url may not be empty"); + final URI originalUri = URI.create(url); + return originalUri.getHost(); + } + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java deleted file mode 100644 index 2d26df89..00000000 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2015-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.cloud.openfeign.security; - -import java.util.Arrays; - -import feign.RequestInterceptor; -import feign.RequestTemplate; - -import org.springframework.security.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.client.http.AccessTokenRequiredException; -import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; -import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; -import org.springframework.security.oauth2.client.token.AccessTokenProvider; -import org.springframework.security.oauth2.client.token.AccessTokenProviderChain; -import org.springframework.security.oauth2.client.token.AccessTokenRequest; -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; -import org.springframework.security.oauth2.common.OAuth2AccessToken; - -/** - * Pre-defined custom RequestInterceptor for Feign Requests. It uses the - * {@link OAuth2ClientContext OAuth2ClientContext} provided from the environment and - * construct a new header on the request before it is made by Feign. - * - * @author Joao Pedro Evangelista - * @author Tim Ysewyn - * @since 3.0.0 - */ -@Deprecated // spring-security-oauth2 reached EOL -public class OAuth2FeignRequestInterceptor implements RequestInterceptor { - - /** - * The name of the token. - */ - public static final String BEARER = "Bearer"; - - /** - * The name of the header. - */ - public static final String AUTHORIZATION = "Authorization"; - - private final OAuth2ClientContext oAuth2ClientContext; - - private final OAuth2ProtectedResourceDetails resource; - - private final String tokenType; - - private final String header; - - private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.asList( - new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(), - new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider())); - - /** - * Default constructor which uses the provided OAuth2ClientContext and Bearer tokens - * within Authorization header. - * @param oAuth2ClientContext provided context - * @param resource type of resource to be accessed - */ - public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource) { - this(oAuth2ClientContext, resource, BEARER, AUTHORIZATION); - } - - /** - * Fully customizable constructor for changing token type and header name, in cases of - * Bearer and Authorization is not the default such as "bearer", "authorization". - * @param oAuth2ClientContext current oAuth2 Context - * @param resource type of resource to be accessed - * @param tokenType type of token e.g. "token", "Bearer" - * @param header name of the header e.g. "Authorization", "authorization" - */ - public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource, String tokenType, String header) { - this.oAuth2ClientContext = oAuth2ClientContext; - this.resource = resource; - this.tokenType = tokenType; - this.header = header; - } - - /** - * Create a template with the header of provided name and extracted extract. - * - * @see RequestInterceptor#apply(RequestTemplate) - */ - @Override - public void apply(RequestTemplate template) { - template.header(header); // Clears out the header, no "clear" method available. - template.header(header, extract(tokenType)); - } - - /** - * Extracts the token extract id the access token exists or returning an empty extract - * if there is no one on the context it may occasionally causes Unauthorized response - * since the token extract is empty. - * @param tokenType type name of token - * @return token value from context if it exists otherwise empty String - */ - protected String extract(String tokenType) { - OAuth2AccessToken accessToken = getToken(); - return String.format("%s %s", tokenType, accessToken.getValue()); - } - - /** - * Extract the access token within the request or try to acquire a new one by - * delegating it to {@link #acquireAccessToken()}. - * @return valid token - */ - public OAuth2AccessToken getToken() { - - OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken(); - if (accessToken == null || accessToken.isExpired()) { - try { - accessToken = acquireAccessToken(); - } - catch (UserRedirectRequiredException e) { - oAuth2ClientContext.setAccessToken(null); - String stateKey = e.getStateKey(); - if (stateKey != null) { - Object stateToPreserve = e.getStateToPreserve(); - if (stateToPreserve == null) { - stateToPreserve = "NONE"; - } - oAuth2ClientContext.setPreservedState(stateKey, stateToPreserve); - } - throw e; - } - } - return accessToken; - } - - /** - * Try to acquire the token using a access token provider. - * @return valid access token - * @throws UserRedirectRequiredException in case the user needs to be redirected to an - * approval page or login page - */ - protected OAuth2AccessToken acquireAccessToken() throws UserRedirectRequiredException { - AccessTokenRequest tokenRequest = oAuth2ClientContext.getAccessTokenRequest(); - if (tokenRequest == null) { - throw new AccessTokenRequiredException( - "Cannot find valid context on request for resource '" + resource.getId() + "'.", resource); - } - String stateKey = tokenRequest.getStateKey(); - if (stateKey != null) { - tokenRequest.setPreservedState(oAuth2ClientContext.removePreservedState(stateKey)); - } - OAuth2AccessToken existingToken = oAuth2ClientContext.getAccessToken(); - if (existingToken != null) { - oAuth2ClientContext.setAccessToken(existingToken); - } - OAuth2AccessToken obtainableAccessToken; - obtainableAccessToken = accessTokenProvider.obtainAccessToken(resource, tokenRequest); - if (obtainableAccessToken == null || obtainableAccessToken.getValue() == null) { - throw new IllegalStateException( - " Access token provider returned a null token, which is illegal according to the contract."); - } - oAuth2ClientContext.setAccessToken(obtainableAccessToken); - return obtainableAccessToken; - } - - public void setAccessTokenProvider(AccessTokenProvider accessTokenProvider) { - this.accessTokenProvider = accessTokenProvider; - } - -} 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 deleted file mode 100644 index 980e8e75..00000000 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2015-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.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 deleted file mode 100644 index 74ebbcdc..00000000 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2015-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.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 9b8c2d58..36a2acc8 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 @@ -75,10 +75,10 @@ "defaultValue": "false" }, { - "name": "spring.cloud.openfeign.oauth2.load-balanced", - "type": "java.lang.Boolean", - "description": "Enables load balancing for oauth2 access token provider.", - "defaultValue": "false" + "name": "spring.cloud.openfeign.oauth2.clientId", + "type": "java.lang.String", + "description": "Provides a clientId to be used with OAuth2.", + "defaultValue": "" } ] } 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 222fa3f3..19c615f3 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 @@ -17,8 +17,6 @@ package org.springframework.cloud.openfeign; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; import feign.Target; import org.assertj.core.api.Condition; @@ -92,17 +90,12 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutInterceptors() { runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.specifiedClientIds=feign-client") + "spring.cloud.openfeign.oauth2.clientId=feign-client") .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) .run(ctx -> { assertOauth2AccessTokenInterceptorExists(ctx); - assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, - new ArrayList() { - { - add("feign-client"); - } - }); + assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, "feign-client"); }); } @@ -112,9 +105,9 @@ class FeignAutoConfigurationTests { } private void assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue( - ConfigurableApplicationContext ctx, List expectedValue) { + ConfigurableApplicationContext ctx, String expectedValue) { final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); - assertThat(bean).hasFieldOrPropertyWithValue("specifiedClientIds", expectedValue); + assertThat(bean).hasFieldOrPropertyWithValue("clientId", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { 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 index 767ab515..e69de29b 100644 --- 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 @@ -1,96 +0,0 @@ -/* - * Copyright 2013-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.cloud.openfeign.security; - -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.Disabled; -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", "spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.load-balanced=true" }) -@DirtiesContext -public class AccessTokenProviderWithLoadBalancerInterceptorTests { - - @Autowired - FeignContext context; - - @Autowired - private ConfigurableApplicationContext applicationContext; - - @Test - @Disabled - 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 index d6c55f82..e69de29b 100644 --- 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 @@ -1,96 +0,0 @@ -/* - * Copyright 2013-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.cloud.openfeign.security; - -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.Disabled; -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", "spring.cloud.openfeign.oauth2.enabled=true" }) -@DirtiesContext -public class AccessTokenProviderWithoutLoadBalancerInterceptorTests { - - @Autowired - FeignContext context; - - @Autowired - private ConfigurableApplicationContext applicationContext; - - @Test - @Disabled - 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/MockAccessTokenProvider.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockAccessTokenProvider.java deleted file mode 100644 index d70eb6ba..00000000 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockAccessTokenProvider.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2015-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.cloud.openfeign.security; - -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; -import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException; -import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; -import org.springframework.security.oauth2.client.token.AccessTokenProvider; -import org.springframework.security.oauth2.client.token.AccessTokenRequest; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2RefreshToken; - -/** - * Mocks the access token provider - * - * @author Mihhail Verhovtsov - */ -public class MockAccessTokenProvider implements AccessTokenProvider { - - private OAuth2AccessToken token; - - public MockAccessTokenProvider(OAuth2AccessToken token) { - this.token = token; - } - - @Override - public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails, - AccessTokenRequest accessTokenRequest) - throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException { - return token; - } - - @Override - public boolean supportsResource(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) { - return true; - } - - @Override - public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails, - OAuth2RefreshToken oAuth2RefreshToken, AccessTokenRequest accessTokenRequest) - throws UserRedirectRequiredException { - return null; - } - - @Override - public boolean supportsRefresh(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) { - return false; - } - -} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2AccessToken.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2AccessToken.java deleted file mode 100644 index 2031441d..00000000 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2AccessToken.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2015-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.cloud.openfeign.security; - -import java.util.Date; -import java.util.Map; -import java.util.Set; - -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2RefreshToken; - -/** - * Mocks the OAuth2 access token - * - * @author Mihhail Verhovtsov - */ -public class MockOAuth2AccessToken implements OAuth2AccessToken { - - private String value; - - public MockOAuth2AccessToken(String value) { - this.value = value; - } - - @Override - public Map getAdditionalInformation() { - return null; - } - - @Override - public Set getScope() { - return null; - } - - @Override - public OAuth2RefreshToken getRefreshToken() { - return null; - } - - @Override - public String getTokenType() { - return null; - } - - @Override - public boolean isExpired() { - return false; - } - - @Override - public Date getExpiration() { - return null; - } - - @Override - public int getExpiresIn() { - return 0; - } - - @Override - public String getValue() { - return value; - } - -} 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 deleted file mode 100644 index 1f0aded3..00000000 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2015-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.cloud.openfeign.security; - -import java.util.HashMap; - -import org.springframework.security.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.client.token.AccessTokenRequest; -import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest; -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2AccessToken; - -/** - * Mocks the current client context - * - * @author João Pedro Evangelista - */ -public final class MockOAuth2ClientContext implements OAuth2ClientContext { - - private final String value; - - MockOAuth2ClientContext(String value) { - this.value = value; - } - - @Override - public OAuth2AccessToken getAccessToken() { - return new DefaultOAuth2AccessToken(value); - } - - @Override - public void setAccessToken(OAuth2AccessToken accessToken) { - - } - - @Override - public AccessTokenRequest getAccessTokenRequest() { - DefaultAccessTokenRequest tokenRequest = new DefaultAccessTokenRequest(new HashMap()); - tokenRequest.setExistingToken(new DefaultOAuth2AccessToken(value)); - return tokenRequest; - } - - @Override - public void setPreservedState(String stateKey, Object preservedState) { - - } - - @Override - public Object removePreservedState(String stateKey) { - return null; - } - -} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index 3066bd61..9a825e57 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -17,15 +17,14 @@ package org.springframework.cloud.openfeign.security; import java.time.Instant; -import java.util.HashMap; import feign.Request.HttpMethod; import feign.RequestTemplate; -import org.assertj.core.api.Assertions; +import feign.Target; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; @@ -35,109 +34,107 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.AlternativeJdkIdGenerator; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** + * Tests for {@link OAuth2AccessTokenInterceptor}. + * * @author Dangzhicairang(小水牛) + * @author Olga Maciaszek-Sharma + * */ class OAuth2AccessTokenInterceptorTests { + private final OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock( + OAuth2AuthorizedClientService.class); + + private final OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock( + OAuth2AuthorizedClientManager.class); + private OAuth2AccessTokenInterceptor oAuth2AccessTokenInterceptor; private RequestTemplate requestTemplate; - private OAuth2ClientProperties mockOAuth2ClientProperties; - private static final String DEFAULT_CLIENT_ID = "feign-client"; @BeforeEach void setUp() { - requestTemplate = new RequestTemplate().method(HttpMethod.GET); - - mockOAuth2ClientProperties = mock(OAuth2ClientProperties.class); - given(mockOAuth2ClientProperties.getRegistration()) - .willReturn(new HashMap() { - { - put(DEFAULT_CLIENT_ID, mock(OAuth2ClientProperties.Registration.class)); - } - }); - + Target feignTarget = mock(Target.class); + when(feignTarget.url()).thenReturn("http://test"); + requestTemplate.feignTarget(feignTarget); } @Test - void noTokenAcquired() { - - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); - - OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); - given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(null); - + void shouldThrowExceptionWhenNoTokenAcquired() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); - Assertions.assertThatExceptionOfType(IllegalStateException.class) + assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) - .withMessage("No token acquired, which is illegal according to the contract."); - + .withMessage("OAuth2 token has not been successfully acquired."); } @Test - void validTokenAcquired() { - - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); - - OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); - given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(validTokenOAuth2AuthorizedClient()); - + void shouldAcquireValidToken() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); - Assertions.assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); } @Test - void expireTokenAcquired() { - - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); - - OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); - given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(expiredTokenOAuth2AuthorizedClient()); - + void shouldThrowExceptionWhenExpiredTokenAcquired() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(expiredTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); - Assertions.assertThatExceptionOfType(IllegalStateException.class) + assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) - .withMessage("No token acquired, which is illegal according to the contract."); + .withMessage("OAuth2 token has not been successfully acquired."); } @Test - void acquireTokenFromAuthorizedClient() { - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())) - .willReturn(validTokenOAuth2AuthorizedClient()); + void shouldAcquireTokenFromAuthorizedClient() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("test"), anyString())) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void shouldAcquireValidTokenFromSpecifiedClientId() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("testId"), anyString())) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor("testId", mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); oAuth2AccessTokenInterceptor.apply(requestTemplate); - Assertions.assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); } private OAuth2AccessToken validToken() { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorTests.java deleted file mode 100644 index 828b25b7..00000000 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorTests.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2015-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.cloud.openfeign.security; - -import java.util.Collection; -import java.util.Map; - -import feign.Request.HttpMethod; -import feign.RequestTemplate; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; -import org.springframework.security.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; -import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; -import org.springframework.security.oauth2.client.token.AccessTokenRequest; -import org.springframework.security.oauth2.common.OAuth2AccessToken; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author João Pedro Evangelista - * @author Tim Ysewyn - * @author Szymon Linowski - */ -class OAuth2FeignRequestInterceptorTests { - - private OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor; - - private RequestTemplate requestTemplate; - - @BeforeEach - void setUp() { - oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(new MockOAuth2ClientContext("Fancy"), - new BaseOAuth2ProtectedResourceDetails()); - requestTemplate = new RequestTemplate().method(HttpMethod.GET); - } - - @Test - void applyAuthorizationHeader() { - oAuth2FeignRequestInterceptor.apply(requestTemplate); - Map> headers = requestTemplate.headers(); - - assertThat(headers.containsKey("Authorization")).describedAs("RequestTemplate must have a Authorization header") - .isTrue(); - Assertions.assertThat(headers.get("Authorization")).describedAs("Authorization must have a extract of Fancy") - .contains("Bearer Fancy"); - } - - @Test - void tryToAcquireToken() { - oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), - new BaseOAuth2ProtectedResourceDetails()); - - Assertions.assertThatExceptionOfType(OAuth2AccessDeniedException.class) - .isThrownBy(() -> oAuth2FeignRequestInterceptor.getToken()).withMessage( - "Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it."); - } - - @Test - void configureAccessTokenProvider() { - OAuth2AccessToken mockedToken = new MockOAuth2AccessToken("MOCKED_TOKEN"); - oAuth2FeignRequestInterceptor.setAccessTokenProvider(new MockAccessTokenProvider(mockedToken)); - - assertThat(oAuth2FeignRequestInterceptor.acquireAccessToken()) - .describedAs("Should return same mocked token instance").isEqualTo(mockedToken); - } - - @Test - void applyAuthorizationHeaderOnlyOnce() { - OAuth2ClientContext oAuth2ClientContext = mock(OAuth2ClientContext.class); - when(oAuth2ClientContext.getAccessToken()).thenReturn(new MockOAuth2AccessToken("MOCKED_TOKEN")); - - OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor( - oAuth2ClientContext, new BaseOAuth2ProtectedResourceDetails()); - - oAuth2FeignRequestInterceptor.apply(requestTemplate); - - // First idempotent call failed, retry mechanism kicks in, and token has expired - // in the meantime - - OAuth2AccessToken expiredAccessToken = mock(OAuth2AccessToken.class); - when(expiredAccessToken.isExpired()).thenReturn(true); - when(oAuth2ClientContext.getAccessToken()).thenReturn(expiredAccessToken); - AccessTokenRequest accessTokenRequest = mock(AccessTokenRequest.class); - when(oAuth2ClientContext.getAccessTokenRequest()).thenReturn(accessTokenRequest); - OAuth2AccessToken newToken = new MockOAuth2AccessToken("Fancy"); - oAuth2FeignRequestInterceptor.setAccessTokenProvider(new MockAccessTokenProvider(newToken)); - - oAuth2FeignRequestInterceptor.apply(requestTemplate); - - Map> headers = requestTemplate.headers(); - assertThat(headers.containsKey("Authorization")).describedAs("RequestTemplate must have a Authorization header") - .isTrue(); - assertThat(headers.get("Authorization")).describedAs("Authorization must have a extract of Fancy").hasSize(1); - assertThat(headers.get("Authorization")).describedAs("Authorization must have a extract of Fancy") - .contains("Bearer Fancy"); - } - -} diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index 98cd1aa5..86f601a4 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -17,22 +17,9 @@ 11.8 3.8.0 - - 2.5.2 - 6.0.0-SNAPSHOT - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - ${spring-security-oauth2-autoconfigure.version} - - - org.springframework.security - spring-security-oauth2-client - ${spring-security-oauth2-client.version} - org.springframework.cloud spring-cloud-openfeign-core From c35fcffa48d46df21ac23863643d7566ad6711d4 Mon Sep 17 00:00:00 2001 From: dzcr <1137729123@qq.com> Date: Fri, 30 Sep 2022 11:13:43 +0800 Subject: [PATCH 3/4] Adjust based on review. --- .../main/asciidoc/spring-cloud-openfeign.adoc | 4 +- .../openfeign/FeignAutoConfiguration.java | 4 +- .../OAuth2AccessTokenInterceptor.java | 45 ++++--------- .../FeignAutoConfigurationTests.java | 4 +- .../OAuth2AccessTokenInterceptorTests.java | 63 ++++++------------- 5 files changed, 39 insertions(+), 81 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index bebace63..625d69b5 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -810,9 +810,9 @@ OAuth2 support can be enabled by setting following flag: spring.cloud.openfeign.oauth2.enabled=true ---- When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. -`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientId` using the `spring.cloud.openfeign.oauth2.clientId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. +`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientRegistrationId` using the `spring.cloud.openfeign.oauth2.clientRegistrationId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientRegistrationId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. -TIP:: Using the `serviceId` as OAuth2 client id is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientId` is a suitable approach. +TIP:: Using the `serviceId` as OAuth2 client registrationId is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientRegistrationId` is a suitable approach. === Transform the load-balanced HTTP request 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 653e5157..0ab2ae95 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 @@ -347,10 +347,10 @@ public class FeignAutoConfiguration { @Bean @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( - @Value("${spring.cloud.openfeign.oauth2.clientId:}") String clientId, + @Value("${spring.cloud.openfeign.oauth2.clientRegistrationId:}") String clientRegistrationId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - return new OAuth2AccessTokenInterceptor(clientId, oAuth2AuthorizedClientService, + return new OAuth2AccessTokenInterceptor(clientRegistrationId, oAuth2AuthorizedClientService, clientRegistrationRepository); } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index b421d0ae..dacd8b30 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -17,7 +17,6 @@ package org.springframework.cloud.openfeign.security; import java.net.URI; -import java.time.Instant; import java.util.Optional; import feign.RequestInterceptor; @@ -69,9 +68,7 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private final String header; - private final String clientId; - - private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + private final String clientRegistrationId; private OAuth2AuthorizedClientManager authorizedClientManager; @@ -87,20 +84,20 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { this(null, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + public OAuth2AccessTokenInterceptor(String clientRegistrationId, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - this(BEARER, AUTHORIZATION, clientId, oAuth2AuthorizedClientService, clientRegistrationRepository); + this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientId, + public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientRegistrationId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { this.tokenType = tokenType; this.header = header; - this.clientId = clientId; - this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; + this.clientRegistrationId = clientRegistrationId; this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, this.oAuth2AuthorizedClientService); + clientRegistrationRepository, oAuth2AuthorizedClientService); } @Override @@ -113,8 +110,8 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { public OAuth2AccessToken getToken(RequestTemplate template) { // If specified, try to use them to get token. - if (StringUtils.hasText(clientId)) { - OAuth2AccessToken token = getToken(clientId); + if (StringUtils.hasText(clientRegistrationId)) { + OAuth2AccessToken token = getToken(clientRegistrationId); if (token != null) { return token; } @@ -129,8 +126,8 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { throw new IllegalStateException("OAuth2 token has not been successfully acquired."); } - protected OAuth2AccessToken getToken(String clientId) { - if (!StringUtils.hasText(clientId)) { + protected OAuth2AccessToken getToken(String clientRegistrationId) { + if (!StringUtils.hasText(clientRegistrationId)) { return null; } @@ -139,26 +136,10 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { principal = ANONYMOUS_AUTHENTICATION; } - // Already exist - OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId, - principal.getName()); - if (oAuth2AuthorizedClient != null) { - OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken(); - if (accessToken != null && notExpired(accessToken)) { - return accessToken; - } - } - - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientId) + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId) .principal(principal).build(); OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); - return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken) - .filter(this::notExpired).orElse(null); - } - - protected boolean notExpired(OAuth2AccessToken token) { - return Optional.ofNullable(token).map(OAuth2AccessToken::getExpiresAt) - .map(expire -> expire.isAfter(Instant.now())).orElse(false); + return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken).orElse(null); } private static String getServiceId(RequestTemplate template) { 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 19c615f3..222ea608 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 @@ -90,7 +90,7 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutInterceptors() { runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.clientId=feign-client") + "spring.cloud.openfeign.oauth2.clientRegistrationId=feign-client") .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) .run(ctx -> { @@ -107,7 +107,7 @@ class FeignAutoConfigurationTests { private void assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue( ConfigurableApplicationContext ctx, String expectedValue) { final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); - assertThat(bean).hasFieldOrPropertyWithValue("clientId", expectedValue); + assertThat(bean).hasFieldOrPropertyWithValue("clientRegistrationId", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index 9a825e57..acedec4e 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -32,14 +32,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.util.AlternativeJdkIdGenerator; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -52,6 +49,9 @@ import static org.mockito.Mockito.when; */ class OAuth2AccessTokenInterceptorTests { + private final ClientRegistrationRepository mockClientRegistrationRepository = mock( + ClientRegistrationRepository.class); + private final OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock( OAuth2AuthorizedClientService.class); @@ -62,7 +62,7 @@ class OAuth2AccessTokenInterceptorTests { private RequestTemplate requestTemplate; - private static final String DEFAULT_CLIENT_ID = "feign-client"; + private static final String DEFAULT_CLIENT_REGISTRATION_ID = "feign-client"; @BeforeEach void setUp() { @@ -74,9 +74,8 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldThrowExceptionWhenNoTokenAcquired() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); + mockClientRegistrationRepository); when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); @@ -87,12 +86,9 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidToken() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); - when(mockOAuth2AuthorizedClientManager.authorize( - argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) - .thenReturn(validTokenOAuth2AuthorizedClient()); + mockClientRegistrationRepository); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -101,25 +97,14 @@ class OAuth2AccessTokenInterceptorTests { } @Test - void shouldThrowExceptionWhenExpiredTokenAcquired() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + void shouldAcquireValidTokenFromServiceId() { oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); - when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(expiredTokenOAuth2AuthorizedClient()); + mockClientRegistrationRepository); + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) - .withMessage("OAuth2 token has not been successfully acquired."); - } - - @Test - void shouldAcquireTokenFromAuthorizedClient() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("test"), anyString())) - .thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); - oAuth2AccessTokenInterceptor.apply(requestTemplate); assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); @@ -127,10 +112,12 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidTokenFromSpecifiedClientId() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("testId"), anyString())) - .thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor("testId", mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, + mockOAuth2AuthorizedClientService, mockClientRegistrationRepository); + when(mockOAuth2AuthorizedClientManager + .authorize(argThat((OAuth2AuthorizeRequest request) -> (DEFAULT_CLIENT_REGISTRATION_ID) + .equals(request.getClientRegistrationId())))).thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -142,23 +129,13 @@ class OAuth2AccessTokenInterceptorTests { Instant.now().plusSeconds(60L)); } - private OAuth2AccessToken expiredToken() { - return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Expired Token", - Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L)); - } - private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() { return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", validToken()); } - private OAuth2AuthorizedClient expiredTokenOAuth2AuthorizedClient() { - return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", expiredToken()); - } - private ClientRegistration defaultClientRegistration() { - return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId().toString()) - .clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri") - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + return ClientRegistration.withRegistrationId(DEFAULT_CLIENT_REGISTRATION_ID).clientId("clientId") + .tokenUri("mock token uri").authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); } } From 09939b61749e7e3e1d7aa35b7fa23d29a41129a0 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Fri, 30 Sep 2022 14:30:30 +0200 Subject: [PATCH 4/4] Inject OAuth2AuthorizedClientManager via constructor. --- .../main/asciidoc/spring-cloud-openfeign.adoc | 4 +- .../openfeign/FeignAutoConfiguration.java | 18 ++++++-- .../OAuth2AccessTokenInterceptor.java | 41 ++++++++----------- ...viderWithLoadBalancerInterceptorTests.java | 0 ...erWithoutLoadBalancerInterceptorTests.java | 0 .../OAuth2AccessTokenInterceptorTests.java | 29 ++++--------- 6 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java delete mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 625d69b5..e756a7ad 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -810,10 +810,12 @@ OAuth2 support can be enabled by setting following flag: spring.cloud.openfeign.oauth2.enabled=true ---- When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. -`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientRegistrationId` using the `spring.cloud.openfeign.oauth2.clientRegistrationId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientRegistrationId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. +`OAuth2AccessTokenInterceptor` uses the `OAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientRegistrationId` using the `spring.cloud.openfeign.oauth2.clientRegistrationId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientRegistrationId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. TIP:: Using the `serviceId` as OAuth2 client registrationId is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientRegistrationId` is a suitable approach. +TIP:: If you do not want to use the default setup for the `OAuth2AuthorizedClientManager`, you can just instantiate a bean of this type in your configuration. + === Transform the load-balanced HTTP request You can use the selected `ServiceInstance` to transform the load-balanced HTTP Request. 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 0ab2ae95..7d262a51 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 @@ -69,6 +69,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -346,12 +347,21 @@ public class FeignAutoConfiguration { @Bean @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) + @ConditionalOnMissingBean + OAuth2AuthorizedClientManager feignOAuth2AuthorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { + return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, + oAuth2AuthorizedClientService); + + } + + @Bean + @ConditionalOnBean(OAuth2AuthorizedClientManager.class) public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( @Value("${spring.cloud.openfeign.oauth2.clientRegistrationId:}") String clientRegistrationId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - return new OAuth2AccessTokenInterceptor(clientRegistrationId, oAuth2AuthorizedClientService, - clientRegistrationRepository); + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + return new OAuth2AccessTokenInterceptor(clientRegistrationId, oAuth2AuthorizedClientManager); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index dacd8b30..c113e244 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -31,8 +31,6 @@ import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2A import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,12 +39,13 @@ import org.springframework.util.StringUtils; * A {@link RequestInterceptor} for OAuth2 Feign Requests. By default, it uses the * {@link AuthorizedClientServiceOAuth2AuthorizedClientManager } to get * {@link OAuth2AuthorizedClient } that holds an {@link OAuth2AccessToken }. If the user - * has specified an OAuth2 {@code clientId} using the - * {@code spring.cloud.openfeign.oauth2.clientId} property, it will be used to retrieve - * the token. If the token is not retrieved or the {@code clientId} has not been - * specified, the {@code serviceId} retrieved from the {@code url} host segment will be - * used. This approach is convenient for load-balanced Feign clients. For - * non-load-balanced ones, the property-based {@code clientId} is a suitable approach. + * has specified an OAuth2 {@code clientRegistrationId} using the + * {@code spring.cloud.openfeign.oauth2.clientRegistrationId} property, it will be used to + * retrieve the token. If the token is not retrieved or the {@code clientRegistrationId} + * has not been specified, the {@code serviceId} retrieved from the {@code url} host + * segment will be used. This approach is convenient for load-balanced Feign clients. For + * non-load-balanced ones, the property-based {@code clientRegistrationId} is a suitable + * approach. * * @author Dangzhicairang(小水牛) * @author Olga Maciaszek-Sharma @@ -70,34 +69,26 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private final String clientRegistrationId; - private OAuth2AuthorizedClientManager authorizedClientManager; - - public void setAuthorizedClientManager(OAuth2AuthorizedClientManager authorizedClientManager) { - this.authorizedClientManager = authorizedClientManager; - } + private final OAuth2AuthorizedClientManager authorizedClientManager; private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - this(null, oAuth2AuthorizedClientService, clientRegistrationRepository); + public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this(null, oAuth2AuthorizedClientManager); } public OAuth2AccessTokenInterceptor(String clientRegistrationId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientService, clientRegistrationRepository); + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientManager); } public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientRegistrationId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { this.tokenType = tokenType; this.header = header; this.clientRegistrationId = clientRegistrationId; - this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, oAuth2AuthorizedClientService); + this.authorizedClientManager = oAuth2AuthorizedClientManager; } @Override @@ -144,9 +135,9 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private static String getServiceId(RequestTemplate template) { Target feignTarget = template.feignTarget(); - Assert.notNull(feignTarget, "feignTarget may not be null"); + Assert.notNull(feignTarget, "FeignTarget may not be null."); String url = feignTarget.url(); - Assert.hasLength(url, "url may not be empty"); + Assert.hasLength(url, "Url may not be empty."); final URI originalUri = URI.create(url); return originalUri.getHost(); } 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 deleted file mode 100644 index e69de29b..00000000 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 deleted file mode 100644 index e69de29b..00000000 diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index acedec4e..686ebab5 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -27,9 +27,7 @@ import org.junit.jupiter.api.Test; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -49,12 +47,6 @@ import static org.mockito.Mockito.when; */ class OAuth2AccessTokenInterceptorTests { - private final ClientRegistrationRepository mockClientRegistrationRepository = mock( - ClientRegistrationRepository.class); - - private final OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock( - OAuth2AuthorizedClientService.class); - private final OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock( OAuth2AuthorizedClientManager.class); @@ -74,10 +66,8 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldThrowExceptionWhenNoTokenAcquired() { - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mockClientRegistrationRepository); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) @@ -86,10 +76,10 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidToken() { - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mockClientRegistrationRepository); - when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -98,12 +88,10 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidTokenFromServiceId() { - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mockClientRegistrationRepository); when(mockOAuth2AuthorizedClientManager.authorize( argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) .thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -111,13 +99,12 @@ class OAuth2AccessTokenInterceptorTests { } @Test - void shouldAcquireValidTokenFromSpecifiedClientId() { + void shouldAcquireValidTokenFromSpecifiedClientRegistrationId() { oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, - mockOAuth2AuthorizedClientService, mockClientRegistrationRepository); + mockOAuth2AuthorizedClientManager); when(mockOAuth2AuthorizedClientManager .authorize(argThat((OAuth2AuthorizeRequest request) -> (DEFAULT_CLIENT_REGISTRATION_ID) .equals(request.getClientRegistrationId())))).thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate);