From d501cbbd07fed010f933f9e9c111bc3c140cc476 Mon Sep 17 00:00:00 2001 From: dzcr <1137729123@qq.com> Date: Fri, 23 Sep 2022 17:28:16 +0800 Subject: [PATCH] step 1 --- spring-cloud-openfeign-core/pom.xml | 4 + .../openfeign/FeignAutoConfiguration.java | 21 ++- .../OAuth2AccessTokenInterceptor.java | 177 ++++++++++++++++++ .../FeignAutoConfigurationTests.java | 67 +++++-- .../OAuth2AccessTokenInterceptorTests.java | 170 +++++++++++++++++ spring-cloud-openfeign-dependencies/pom.xml | 6 + 6 files changed, 429 insertions(+), 16 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 863e314c..b441393d 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -225,6 +225,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 857f7f75..ec97c25d 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 @@ -51,6 +51,7 @@ 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; @@ -62,6 +63,7 @@ import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionMa 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.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; @@ -74,7 +76,9 @@ 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.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; import static org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder.buildWithConfigurers; @@ -91,6 +95,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) @@ -369,12 +374,24 @@ public class FeignAutoConfiguration { @Bean @ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class) - @ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class }) + @ConditionalOnBean({OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class}) public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource, List configurers) { + OAuth2ProtectedResourceDetails resource, List configurers) { return buildWithConfigurers(oAuth2ClientContext, resource, configurers); } + @Bean + @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..cc017006 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -0,0 +1,177 @@ +/* + * 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 b906ff55..4997f29e 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; @@ -29,12 +31,15 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver; import org.springframework.cloud.openfeign.security.MockOAuth2ClientContext; +import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; import static org.assertj.core.api.Assertions.assertThat; @@ -46,6 +51,7 @@ import static org.mockito.Mockito.mock; * @author Andrii Bohutskyi * @author Kwangyong Kim * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ class FeignAutoConfigurationTests { @@ -123,12 +129,30 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithCustomAccessTokenProviderInterceptor() { - runner.withPropertyValues("feign.oauth2.enabled=true").withBean(MockOAuth2ClientContext.class, "token") - .withBean(BaseOAuth2ProtectedResourceDetails.class) - .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); - }); + runner.withPropertyValues("feign.oauth2.enabled=true") + .withBean(MockOAuth2ClientContext.class, "token") + .withBean(BaseOAuth2ProtectedResourceDetails.class) + .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); + }); + } + + @Test + void shouldInstantiateFeignOAuth2FeignRequestInterceptor() { + runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", + "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"); + } + }); + }); } private void assertOauth2FeignRequestInterceptorExists(ConfigurableApplicationContext ctx) { @@ -137,28 +161,43 @@ class FeignAutoConfigurationTests { } private void assertAccessTokenProviderInterceptorExists(ConfigurableApplicationContext ctx, - Class clazz) { + Class clazz) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class) + .extracting("accessTokenProvider") .extracting("interceptors").asList().first().isInstanceOf(clazz); } private void assertAccessTokenProviderInterceptorNotExists(ConfigurableApplicationContext ctx, - Class clazz) { + Class clazz) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") - .extracting("interceptors").asList().filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) - .isEmpty(); + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class) + .extracting("accessTokenProvider") + .extracting("interceptors").asList() + .filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) + .isEmpty(); + } + + private void assertOauth2AccessTokenInterceptorExists(ConfigurableApplicationContext ctx) { + AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); + assertThat(context).hasSingleBean(OAuth2AccessTokenInterceptor.class); + } + + 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) { - assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1).hasValueSatisfying(new Condition<>( + assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1) + .hasValueSatisfying(new Condition<>( beanClass::isInstance, String.format("Targeter should be an instance of %s", beanClass))); } private void assertThatFeignCircuitBreakerTargeterHasGroupEnabledPropertyWithValue( - ConfigurableApplicationContext ctx, boolean expectedValue) { + ConfigurableApplicationContext ctx, boolean expectedValue) { final FeignCircuitBreakerTargeter bean = ctx.getBean(FeignCircuitBreakerTargeter.class); assertThat(bean).hasFieldOrPropertyWithValue("circuitBreakerGroupEnabled", expectedValue); } 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..4242609b --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -0,0 +1,170 @@ +/* + * 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 16605ed7..a46020b6 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