Refactor. Add docs. Fix dependencies.
This commit is contained in:
@@ -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>
|
||||
@@ -218,6 +199,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<profiles>
|
||||
|
||||
@@ -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<String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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<String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<String>() {
|
||||
{
|
||||
add("feign-client");
|
||||
}
|
||||
});
|
||||
assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, "feign-client");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,9 +105,9 @@ class FeignAutoConfigurationTests {
|
||||
}
|
||||
|
||||
private void assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(
|
||||
ConfigurableApplicationContext ctx, List<String> 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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, OAuth2ClientProperties.Registration>() {
|
||||
{
|
||||
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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user