From e9777e5d2e63ce28e4b7108c99ef2689778c35c0 Mon Sep 17 00:00:00 2001 From: buildmaster Date: Wed, 4 Sep 2024 16:55:00 +0000 Subject: [PATCH 1/2] Bumping versions --- docs/modules/ROOT/partials/_configprops.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 0448844a..2819a12c 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -65,7 +65,7 @@ |spring.cloud.loadbalancer.subset.size | `+++100+++` | Max subset size of deterministic subsetting. |spring.cloud.loadbalancer.x-forwarded.enabled | `+++false+++` | To Enable X-Forwarded Headers. |spring.cloud.openfeign.autoconfiguration.jackson.enabled | `+++true+++` | If true, PageJacksonModule and SortJacksonModule bean will be provided for Jackson page decoding. -|spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled | `+++false+++` | If true, Circuit Breaker ids will only contain alphanumeric characters to allow for configuration via configuration properties. +|spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled | `+++true+++` | If true, Circuit Breaker ids will only contain alphanumeric characters to allow for configuration via configuration properties. |spring.cloud.openfeign.circuitbreaker.enabled | `+++false+++` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker. |spring.cloud.openfeign.circuitbreaker.group.enabled | `+++false+++` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with group. |spring.cloud.openfeign.client.config | | From c1988b824cb29fef6c4158c0537d5502da413e87 Mon Sep 17 00:00:00 2001 From: Philipp Meier Date: Thu, 5 Sep 2024 18:20:08 +0200 Subject: [PATCH 2/2] Authorize with anonymous principal if principal name is null (#1050) The authorizedClientManager.authorize method requires a non-null principal name or it will usually throw an exception in practice like "principalName cannot be empty". Using the anonymous principal in this case like for a null principal handles the situation more gracefully. Fixes #1049 --- .../OAuth2AccessTokenInterceptor.java | 5 +- .../OAuth2AccessTokenInterceptorTests.java | 69 +++++++++++++++++-- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index 07d4697e..1fd5b58f 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2024 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. @@ -48,6 +48,7 @@ import org.springframework.util.StringUtils; * * @author Dangzhicairang(小水牛) * @author Olga Maciaszek-Sharma + * @author Philipp Meier * @since 4.0.0 */ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { @@ -122,7 +123,7 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { } Authentication principal = SecurityContextHolder.getContext().getAuthentication(); - if (principal == null) { + if (principal == null || principal.getName() == null) { principal = ANONYMOUS_AUTHENTICATION; } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index cec3fe95..9883d33a 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2024 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. @@ -23,7 +23,10 @@ import feign.RequestTemplate; import feign.Target; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; @@ -43,6 +46,7 @@ import static org.mockito.Mockito.when; * * @author Dangzhicairang(小水牛) * @author Olga Maciaszek-Sharma + * @author Philipp Meier * */ class OAuth2AccessTokenInterceptorTests { @@ -78,7 +82,7 @@ class OAuth2AccessTokenInterceptorTests { void shouldAcquireValidToken() { oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); when(mockOAuth2AuthorizedClientManager - .authorize(argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .authorize(argThat(matchAuthorizeRequest("test")))) .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -89,7 +93,7 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidTokenFromServiceId() { when(mockOAuth2AuthorizedClientManager - .authorize(argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .authorize(argThat(matchAuthorizeRequest("test")))) .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); @@ -99,12 +103,12 @@ class OAuth2AccessTokenInterceptorTests { } @Test - void shouldAcquireValidTokenFromSpecifiedClientRegistrationId() { + void shouldAcquireValidTokenFromSpecifiedClientRegistrationIdPrincipalIsNull() { + SecurityContextHolder.getContext().setAuthentication(null); oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, mockOAuth2AuthorizedClientManager); when(mockOAuth2AuthorizedClientManager - .authorize(argThat((OAuth2AuthorizeRequest request) -> (DEFAULT_CLIENT_REGISTRATION_ID) - .equals(request.getClientRegistrationId())))) + .authorize(argThat(matchAuthorizeRequest(DEFAULT_CLIENT_REGISTRATION_ID)))) .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -112,13 +116,55 @@ class OAuth2AccessTokenInterceptorTests { assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); } + @Test + void shouldAcquireValidTokenFromSpecifiedClientRegistrationIdPrincipalNameIsNull() { + SecurityContextHolder.getContext().setAuthentication(principalWithName(null)); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, + mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager + .authorize(argThat(matchAuthorizeRequest(DEFAULT_CLIENT_REGISTRATION_ID)))) + .thenReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void shouldAcquireValidTokenFromSpecifiedClientRegistrationIdPrincipalNameIsNotNull() { + String principalName = "principalName"; + SecurityContextHolder.getContext().setAuthentication(principalWithName(principalName)); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, + mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager + .authorize(argThat(matchAuthorizeRequestWithPrincipalName(DEFAULT_CLIENT_REGISTRATION_ID, principalName)))) + .thenReturn(validTokenOAuth2AuthorizedClienWithPrincipalName(principalName)); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + private ArgumentMatcher matchAuthorizeRequest(String clientRegistrationId) { + return matchAuthorizeRequestWithPrincipalName(clientRegistrationId, "anonymousUser"); + } + + private ArgumentMatcher matchAuthorizeRequestWithPrincipalName(String clientRegistrationId, String principalName) { + return (OAuth2AuthorizeRequest request) -> clientRegistrationId.equals(request.getClientRegistrationId()) + && principalName.equals(request.getPrincipal().getName()); + } + 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()); + return validTokenOAuth2AuthorizedClienWithPrincipalName("anonymousUser"); + } + + private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClienWithPrincipalName(String principalName) { + return new OAuth2AuthorizedClient(defaultClientRegistration(), principalName, validToken()); } private ClientRegistration defaultClientRegistration() { @@ -129,4 +175,13 @@ class OAuth2AccessTokenInterceptorTests { .build(); } + private TestingAuthenticationToken principalWithName(String principalName) { + return new TestingAuthenticationToken(new java.security.Principal() { + @Override + public String getName() { + return principalName; + } + }, null); + } + }