Browse Source

Merge pull request #759 from spring-cloud/migrate-oauth-support-new

Migrate oauth support new
pull/764/head
Olga Maciaszek-Sharma 2 years ago committed by GitHub
parent
commit
6098d910ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      docs/src/main/asciidoc/spring-cloud-openfeign.adoc
  2. 24
      spring-cloud-openfeign-core/pom.xml
  3. 49
      spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java
  4. 145
      spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java
  5. 183
      spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java
  6. 81
      spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java
  7. 35
      spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java
  8. 8
      spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json
  9. 84
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java
  10. 96
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java
  11. 96
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java
  12. 65
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockAccessTokenProvider.java
  13. 79
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2AccessToken.java
  14. 67
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java
  15. 128
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java
  16. 118
      spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorTests.java
  17. 7
      spring-cloud-openfeign-dependencies/pom.xml

11
docs/src/main/asciidoc/spring-cloud-openfeign.adoc

@ -809,11 +809,12 @@ OAuth2 support can be enabled by setting following flag: @@ -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

24
spring-cloud-openfeign-core/pom.xml

@ -132,25 +132,6 @@ @@ -132,25 +132,6 @@
<artifactId>okhttp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
@ -215,6 +196,11 @@ @@ -215,6 +196,11 @@
<version>2.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<profiles>
<profile>

49
spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java

@ -29,7 +29,6 @@ import com.fasterxml.jackson.databind.Module; @@ -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; @@ -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; @@ -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 @@ -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 { @@ -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<OAuth2FeignRequestInterceptorConfigurer> 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);
}
}

145
spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java

@ -0,0 +1,145 @@ @@ -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();
}
}

183
spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java

@ -1,183 +0,0 @@ @@ -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.<AccessTokenProvider>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;
}
}

81
spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java

@ -1,81 +0,0 @@ @@ -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<ClientHttpRequestInterceptor> accessTokenProviderInterceptors = new ArrayList<>();
public OAuth2FeignRequestInterceptorBuilder() {
accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider>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<OAuth2FeignRequestInterceptorConfigurer> buildConfigurers) {
final OAuth2FeignRequestInterceptorBuilder builder = new OAuth2FeignRequestInterceptorBuilder();
for (OAuth2FeignRequestInterceptorConfigurer configurer : buildConfigurers) {
configurer.customize(builder);
}
return builder.build(oAuth2ClientContext, resource);
}
}

35
spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java

@ -1,35 +0,0 @@ @@ -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);
}

8
spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json

@ -75,10 +75,10 @@ @@ -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": ""
}
]
}

84
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java

@ -26,16 +26,11 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -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; @@ -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 { @@ -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<? extends ClientHttpRequestInterceptor> 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<? extends ClientHttpRequestInterceptor> 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 { @@ -179,14 +137,4 @@ class FeignAutoConfigurationTests {
}
static class CustomOAuth2FeignRequestInterceptorConfigurer implements OAuth2FeignRequestInterceptorConfigurer {
@Override
public void customize(OAuth2FeignRequestInterceptorBuilder requestInterceptorBuilder) {
requestInterceptorBuilder
.withAccessTokenProviderInterceptors(new BasicAuthenticationInterceptor("username", "password"));
}
}
}

96
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java

@ -1,96 +0,0 @@ @@ -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();
}
}
}

96
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java

@ -1,96 +0,0 @@ @@ -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();
}
}
}

65
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockAccessTokenProvider.java

@ -1,65 +0,0 @@ @@ -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;
}
}

79
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2AccessToken.java

@ -1,79 +0,0 @@ @@ -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<String, Object> getAdditionalInformation() {
return null;
}
@Override
public Set<String> 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;
}
}

67
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/MockOAuth2ClientContext.java

@ -1,67 +0,0 @@ @@ -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<String, String[]>());
tokenRequest.setExistingToken(new DefaultOAuth2AccessToken(value));
return tokenRequest;
}
@Override
public void setPreservedState(String stateKey, Object preservedState) {
}
@Override
public Object removePreservedState(String stateKey) {
return null;
}
}

128
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java

@ -0,0 +1,128 @@ @@ -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();
}
}

118
spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorTests.java

@ -1,118 +0,0 @@ @@ -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<String, Collection<String>> 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<String, Collection<String>> 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");
}
}

7
spring-cloud-openfeign-dependencies/pom.xml

@ -17,16 +17,9 @@ @@ -17,16 +17,9 @@
<properties>
<feign.version>11.8</feign.version>
<feign-form.version>3.8.0</feign-form.version>
<!-- Deprecated - reached EOL -->
<spring-security-oauth2-autoconfigure.version>2.5.2</spring-security-oauth2-autoconfigure.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring-security-oauth2-autoconfigure.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>

Loading…
Cancel
Save