Refactor. Add docs. Fix dependencies.

This commit is contained in:
Olga Maciaszek-Sharma
2022-09-27 15:14:54 +02:00
parent d501cbbd07
commit 60fc4a53e7
8 changed files with 150 additions and 152 deletions

View File

@@ -51,7 +51,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;
@@ -383,13 +382,11 @@ public class FeignAutoConfiguration {
@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);
}
}

View File

@@ -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;
@@ -79,69 +80,56 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor {
}
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous",
"anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
"anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
public OAuth2AccessTokenInterceptor(OAuth2ClientProperties oAuth2ClientProperties,
OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
ClientRegistrationRepository clientRegistrationRepository) {
this(new ArrayList<>(), oAuth2ClientProperties, oAuth2AuthorizedClientService, clientRegistrationRepository);
public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
ClientRegistrationRepository clientRegistrationRepository) {
this(null, oAuth2AuthorizedClientService, clientRegistrationRepository);
}
public OAuth2AccessTokenInterceptor(List<String> specifiedClientIds, OAuth2ClientProperties oAuth2ClientProperties,
OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
ClientRegistrationRepository clientRegistrationRepository) {
this(BEARER, AUTHORIZATION, specifiedClientIds, oAuth2ClientProperties, oAuth2AuthorizedClientService,
clientRegistrationRepository);
public OAuth2AccessTokenInterceptor(String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
ClientRegistrationRepository clientRegistrationRepository) {
this(BEARER, AUTHORIZATION, clientId, oAuth2AuthorizedClientService, clientRegistrationRepository);
}
public OAuth2AccessTokenInterceptor(String tokenType, String header, List<String> specifiedClientIds,
OAuth2ClientProperties oAuth2ClientProperties, OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
ClientRegistrationRepository clientRegistrationRepository) {
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);
clientRegistrationRepository, this.oAuth2AuthorizedClientService);
}
@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;
}
@@ -151,27 +139,35 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor {
principal = ANONYMOUS_AUTHENTICATION;
}
// already exist
// Already exist
OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId,
principal.getName());
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);
.principal(principal).build();
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);
.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();
}
}

View File

@@ -79,6 +79,12 @@
"type": "java.lang.Boolean",
"description": "Enables load balancing for oauth2 access token provider.",
"defaultValue": "false"
},
{
"name": "feign.oauth2.registrationClientId",
"type": "java.lang.String",
"description": "Provides a clientId to be used with OAuth2.",
"defaultValue": ""
}
]
}

View File

@@ -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;
@@ -141,17 +139,12 @@ class FeignAutoConfigurationTests {
@Test
void shouldInstantiateFeignOAuth2FeignRequestInterceptor() {
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");
});
}
@@ -184,9 +177,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) {

View File

@@ -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,121 +34,117 @@ 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)
.isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate))
.withMessage("No token acquired, which is illegal according to the contract.");
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate))
.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)
.isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate))
.withMessage("No token acquired, which is illegal according to the contract.");
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate))
.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() {
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Valid Token", Instant.now(),
Instant.now().plusSeconds(60L));
Instant.now().plusSeconds(60L));
}
private OAuth2AccessToken expiredToken() {
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Expired Token",
Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L));
Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L));
}
private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() {
@@ -161,10 +156,9 @@ class OAuth2AccessTokenInterceptorTests {
}
private ClientRegistration defaultClientRegistration() {
return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId()
.toString())
.clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build();
return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId().toString())
.clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build();
}
}