Summary
WebFlux OAuth2 WebClient Authentication in grant_type client_credentials uses the same token from HTTP Server Request context, even if client_id is different.
Example Callstack: Spring Service 1 calls Spring Service 2 via WebClient with Oauth2 JWT-Token in client credentials stacks with client-id "call1"
In the same HTTP Context in Spring Service 2 we are trying to call another REST Spring Service with another OAuth2 JWT-Token and another client_id.
Actual Behavior
Spring tries to use the same OAUTH2 JWT-Token although you want to call another service with different security settings. Especially if the Client_Id is not the same, the original token should not be taken from the HTTP request.
Additionally the class UnAuthenticatedServerOAuth2AuthorizedClientRepository has to be adapted, otherwise an IllegalArgumentException is thrown by the lines 49, 60 and 72: Assert.isNull(serverWebExchange, "serverWebExchange must be null"); - i commented these lines out. Following Exception is thrown, cause the OAUTH2 Provider tries to decrypt a username from the existing token (we are on granttype client_credentials ...):
java.lang.IllegalArgumentException: The user org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken@ffffffc4: Principal: org.springframework.security.oauth2.jwt.Jwt@cb2e98b1; Credentials: [PROTECTED]; Authenticated: true; Details: null; Not granted any authorities should not be authenticated
at org.springframework.util.Assert.isTrue(Assert.java:118)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST https://xxxx [DefaultWebClient]
Expected Behavior
Spring Security with WebFlux can handle muliple HTTP requests to different services with other OAuth2 configurations, even if we are already in HTTP Server Web Request with another Token.
Configuration
WebClient Config on all Services (different clientID and clientSecret):
private WebClient createWebClient() {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
new InMemoryReactiveClientRegistrationRepository(createClientRegistration()),
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultOAuth2AuthorizedClient(false);
return WebClient.builder().filter(oauth).baseUrl(properties.getBaseUrl()).build();
}
private ClientRegistration createClientRegistration() {
return ClientRegistration.withRegistrationId(CLIENT_REGISTRATION_ID)
.tokenUri(properties.getAccessTokenUri())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientId(properties.getClientId())
.clientSecret(properties.getClientSecret())
.build();
Version
Spring Boot 2.2.4
implementation('org.springframework.boot:spring-boot-starter-webflux') implementation('org.springframework.boot:spring-boot-starter-security') implementation('org.springframework.security:spring-security-oauth2-resource-server') implementation('org.springframework.security:spring-security-oauth2-jose')
implementation('org.springframework.security:spring-security-oauth2-client')
Sample
Due to this complex constellation with several services, an example is too extensive. Example snippets are provided.
Comment From: jgrandja
@awilhelmer In order to help troubleshoot, you will need to provide more detail. The information provided is not sufficient. The most effective way for us to help you is to provide a minimal sample that reproduces the issue. I'll wait on that.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.
Comment From: LittleBaiBai
@jgrandja I ran into the same issue recently, and here is my sample project to reproduce it: https://github.com/LittleBaiBai/spring-security-issue-7984-sample. My use case is that service-a authenticates users based on certain criteria, then uses it's own client credentials to call service-b. But the WebClient filter failed to apply because there is already an Authentication object in context.
Was this the intended behavior? Is there a way to bypass this issue?
Comment From: LittleBaiBai
@alek-sys and I found a filter configuration that worked for us (branch problem-solved in the sample repo):
private ExchangeFilterFunction getOAuth2FilterFunction(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
InMemoryReactiveOAuth2AuthorizedClientService authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2FilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService)
);
oauth2FilterFunction.setDefaultClientRegistrationId(API_CLIENT_ID);
return oauth2FilterFunction;
}
Our exploration process goes like this: we were hoping to ignore the existing Authentication object in the context by using UnAuthenticatedServerOAuth2AuthorizedClientRepository, and apparently that would just fail the "unauthenticated" assertion. Then we tried some other implementations of the ServerOAuth2AuthorizedClientRepository but they seem to all require webServerExchange which would work in this sample app, but fail in our code because we also use the same WebClient in an afterPropertiesSet class. We traced it down to the DefaultReactiveOAuth2AuthorizedClientManager class requiring webServerExchange, and that got us to think about using this AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager implementation which was designed to use without a ServerHttpRequest context.
With all this exploration, our biggest surprise was that the default manager implementation require webServerExchange, to us that's ServerOAuth2AuthorizedClientRepository's responsibility. So we were a little confused about the responsibility of each abstraction layer (manager <- service <- repository).
@jgrandja We would love to hear your thoughts on this. Thanks!
Comment From: jgrandja
@LittleBaiBai I tried running ControllerTest in your sample but it failed to start reporting an error with Docker:
Caused by: org.testcontainers.containers.ContainerFetchException: Can't get Docker image: RemoteDockerImage(imageNameFuture=org.testcontainers.images.RemoteDockerImage$1@13f4c286, imagePullPolicy=DefaultPullPolicy(), dockerClient=LazyDockerClient.INSTANCE)
at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1265)
at org.testcontainers.containers.GenericContainer.logger(GenericContainer.java:600)
at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:311)
... 97 more
Caused by: java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
Either way, after looking at your code, this issue does not seem related to the original issue.
ControllerTest.callEndpointA() uses WebTestClient to call TestController.endpointA() without a bearer token. The test uses @WithMockUser, which sets up an UsernamePasswordAuthenticationToken in the SecurityContext. The call proceeds to the WebClient call because the request is authenticated and your security configuration specifies .authorizeExchange().pathMatchers("/test/**").authenticated().
But the WebClient filter failed to apply because there is already an Authentication object in context.
The WebClient filter fails here because you don't explicitly specify a ClientRegistration to use. Please see this sample how to specify ClientRegistration to use for a protected resource request.
- specify OAuth2AuthorizedClient
- specify clientRegistrationId
our biggest surprise was that the default manager implementation require webServerExchange, to us that's ServerOAuth2AuthorizedClientRepository's responsibility
The javadoc clearly states when to use DefaultReactiveOAuth2AuthorizedClientManager and AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.
Also, as an FYI, UnAuthenticatedServerOAuth2AuthorizedClientRepository is deprecated as of 5.3.
I would encourage you to review the OAuth 2.0 Client reference doc to understand the role of the OAuth2AuthorizedClientRepository vs. OAuth2AuthorizedClientManager. The servlet docs are up-to-date but the reactive docs need to be added/updated. Either way, the representation of roles/responsibilities are the same - just different classes.
Comment From: LittleBaiBai
@jgrandja To run the tests you would need docker running for TestContainers to start an UAA server. To avoid using docker, you could also comment out this line and update application-test.yml to point to your local UAA. Apologies for not mentioning that in the README.
The WebClient filter fails here because you don't explicitly specify a ClientRegistration to use.
In the filter function configuration I've called setDefaultClientRegistrationId and that has worked for me in other cases. I also tried setting clientRegistrationId attribute following your example, the callEndpointA test still fails (check branch fail-with-explicit-client-registration-id-attribute).
I believe this sample does represent the original issue. I used @WithMockUser to simplify the test setup, but if I set a Authentication header with WebTestClient in test, I'll get the same exception as described in the original issue.
Since AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager did solve my problem, and UnAuthenticatedServerOAuth2AuthorizedClientRepository is deprecated since 5.3, I'm fine with closing this issue.
Comment From: jgrandja
@LittleBaiBai I re-tested with a local UAA and the issue you are having is actually not related to the original issue. The original issue states:
Spring tries to use the same OAUTH2 JWT-Token although you want to call another service with different security settings
Your test does not pass a JWT bearer token in the initial request. It simply authenticates the initial request using @WithMockUser. After it hits the endpoint, the filter function attempts to UnAuthenticatedServerOAuth2AuthorizedClientRepository.loadAuthorizedClient() to see if client b has previously been authorized and the check to Assert.isTrue(isUnauthenticated(authentication) fails since UnAuthenticatedServerOAuth2AuthorizedClientRepository should only be used/configured outside of an authenticated request. The original issue also reports this in addition to the previous one I mentioned.
Either way, this is a misconfiguration so you just need to use AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager and you should be good to go.
Comment From: LittleBaiBai
Just wanted to give an update with the configuration I posted earlier - with that configuration it doesn't actually set the bearer token requests because the ReactiveOAuth2AuthorizedClientProvider is not set on the manager.
Here is the revised configuration that worked for us:
private ExchangeFilterFunction getOAuth2FilterFunction(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
InMemoryReactiveOAuth2AuthorizedClientService authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(new ClientCredentialsReactiveOAuth2AuthorizedClientProvider());
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2FilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2FilterFunction.setDefaultClientRegistrationId(API_CLIENT_ID);
return oauth2FilterFunction;
}