From 8d3d5c574fe4e86715c302aa4883d41f651f876c Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 27 Sep 2022 15:14:54 +0200 Subject: [PATCH] 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 | 123 ++++++------ .../OAuth2FeignRequestInterceptorTests.java | 118 ----------- spring-cloud-openfeign-dependencies/pom.xml | 13 -- 17 files changed, 126 insertions(+), 992 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()); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + void shouldAcquireTokenFromAuthorizedClient() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("test"), anyString())) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(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"); + } + + @Test + void shouldAcquireValidTokenFromSpecifiedClientId() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("testId"), anyString())) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor("testId", mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + 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