diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 994476dd..e756a7ad 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -809,11 +809,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 `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 `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 diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 6ac76c20..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 @@ -215,6 +196,11 @@ 2.11.0 test + + 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 c40bfb66..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 @@ -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; @@ -55,14 +54,11 @@ 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 +69,10 @@ 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.AuthorizedClientServiceOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; /** * @author Spencer Gibb @@ -90,6 +86,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 +341,27 @@ public class FeignAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(OAuth2ClientContext.class) + @ConditionalOnClass(OAuth2AuthorizedClientManager.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({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) + @ConditionalOnMissingBean + OAuth2AuthorizedClientManager feignOAuth2AuthorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { + return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, + oAuth2AuthorizedClientService); - @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(OAuth2AuthorizedClientManager.class) + public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( + @Value("${spring.cloud.openfeign.oauth2.clientRegistrationId:}") String clientRegistrationId, + 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 new file mode 100644 index 00000000..c113e244 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -0,0 +1,145 @@ +/* + * 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.net.URI; +import java.util.Optional; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import feign.Target; + +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.core.OAuth2AccessToken; +import org.springframework.util.Assert; +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 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 + * @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 String clientRegistrationId; + + private final OAuth2AuthorizedClientManager authorizedClientManager; + + private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", + "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this(null, oAuth2AuthorizedClientManager); + } + + public OAuth2AccessTokenInterceptor(String clientRegistrationId, + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientManager); + } + + public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientRegistrationId, + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this.tokenType = tokenType; + this.header = header; + this.clientRegistrationId = clientRegistrationId; + this.authorizedClientManager = oAuth2AuthorizedClientManager; + } + + @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, extractedToken); + } + + public OAuth2AccessToken getToken(RequestTemplate template) { + // If specified, try to use them to get token. + if (StringUtils.hasText(clientRegistrationId)) { + OAuth2AccessToken token = getToken(clientRegistrationId); + 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("OAuth2 token has not been successfully acquired."); + } + + protected OAuth2AccessToken getToken(String clientRegistrationId) { + if (!StringUtils.hasText(clientRegistrationId)) { + return null; + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + if (principal == null) { + principal = ANONYMOUS_AUTHENTICATION; + } + + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId) + .principal(principal).build(); + OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); + return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken).orElse(null); + } + + 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 bcf67e35..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 @@ -26,16 +26,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 +41,7 @@ import static org.mockito.Mockito.mock; * @author Andrii Bohutskyi * @author Kwangyong Kim * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ class FeignAutoConfigurationTests { @@ -93,63 +89,25 @@ 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.clientRegistrationId=feign-client") + .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) + .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) + .run(ctx -> { + assertOauth2AccessTokenInterceptorExists(ctx); + assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, "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, String expectedValue) { + final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); + assertThat(bean).hasFieldOrPropertyWithValue("clientRegistrationId", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { @@ -179,14 +137,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 deleted file mode 100644 index cec77e98..00000000 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java +++ /dev/null @@ -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 deleted file mode 100644 index 4693a137..00000000 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java +++ /dev/null @@ -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 new file mode 100644 index 00000000..686ebab5 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -0,0 +1,128 @@ +/* + * 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 feign.Request.HttpMethod; +import feign.RequestTemplate; +import feign.Target; +import org.junit.jupiter.api.BeforeEach; +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.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +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.argThat; +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 OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock( + OAuth2AuthorizedClientManager.class); + + private OAuth2AccessTokenInterceptor oAuth2AccessTokenInterceptor; + + private RequestTemplate requestTemplate; + + private static final String DEFAULT_CLIENT_REGISTRATION_ID = "feign-client"; + + @BeforeEach + void setUp() { + requestTemplate = new RequestTemplate().method(HttpMethod.GET); + Target feignTarget = mock(Target.class); + when(feignTarget.url()).thenReturn("http://test"); + requestTemplate.feignTarget(feignTarget); + } + + @Test + void shouldThrowExceptionWhenNoTokenAcquired() { + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("OAuth2 token has not been successfully acquired."); + } + + @Test + void shouldAcquireValidToken() { + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void shouldAcquireValidTokenFromServiceId() { + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void shouldAcquireValidTokenFromSpecifiedClientRegistrationId() { + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, + mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager + .authorize(argThat((OAuth2AuthorizeRequest request) -> (DEFAULT_CLIENT_REGISTRATION_ID) + .equals(request.getClientRegistrationId())))).thenReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + 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 OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() { + return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", validToken()); + } + + private ClientRegistration defaultClientRegistration() { + return ClientRegistration.withRegistrationId(DEFAULT_CLIENT_REGISTRATION_ID).clientId("clientId") + .tokenUri("mock token uri").authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + } + +} 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 9c10c686..86f601a4 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -17,16 +17,9 @@ 11.8 3.8.0 - - 2.5.2 - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - ${spring-security-oauth2-autoconfigure.version} - org.springframework.cloud spring-cloud-openfeign-core