Search code examples
spring-bootspring-securityspring-webclient

How can I ensure that a new token is used for each user when issuing WebClient http requests altered from within an ExchangeFilterFunction?


If you want to get a bit more context regarding this question, know this is a follow-up question to: How can I authenticate using the token exchange grant type for impersonation with spring boot and keycloak?

I need to impersonate through token exchange various users and as such I'm going to be needing a new token for each user. So basically the token exchange authentication query is the same each time but with a new requested_subject parameter. I'm fetching its value for the ClientRequest's attributes. This looks like this right now:

MonoGraphQLClient.createWithWebClient(WebClient.builder()
    .baseUrl(properties.httpUrl)
    .filter { request, next ->
        val userAttribute: String =  request.attribute(REQUESTING_USER_ATTRIBUTE).map(Any::toString).orElseThrow()
        ServerOAuth2AuthorizedClientExchangeFilterFunction(
            AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                clientService
            ).apply {
                setAuthorizedClientProvider(TokenExchangeReactiveOAuth2AuthorizedClientProvider().apply {
                    // Bypassing subject token presence check in TokenExchangeReactiveOAuth2AuthorizedClientProvider::authorize
                    setSubjectTokenResolver { Mono.just(OAuth2Token { null }) }
                    // Overriding client with our more permissive one
                    setAccessTokenResponseClient(LenientWebClientReactiveTokenExchangeTokenResponseClient().apply {
                        parametersProvider = {
                            LinkedMultiValueMap<String, String>().apply {
                                add("audience", properties.tokenAudience)
                                add("requested_subject", userAttribute) // This is where I set it
                            }
                        }
                    })
                })
            }
        ).also {
            // We know we use only one provider, so it's easier and less coupled to the actual value to do things like this
            it.setDefaultClientRegistrationId(clientRegistrationRepository.first().registrationId)
        }.filter(request, next)
    }
    .build()

So the issue here is that on each request it reuses the previous token as long as it's not expired as it's being stored by ReactiveOAuth2AuthorizedClientService. I'd like a new token for each requested_subject.

The thing is, ReactiveOAuth2AuthorizedClientService's methods are based on the clientRegistrationId and principalName and ServerOAuth2AuthorizedClientExchangeFilterFunction, gets the principalName from these lines:

enter image description here

Note that the SecurityContext is not present here so we're always falling back to the anonymous user. The reason is that I'm in a service that reacts to events and does graphQL queries to a third party service. It's completely isolated and not publicly exposed and as such there is no authentication to this service.

So I have a couple solutions in my mind but I'm not really satisfied with any of these.

  1. Recode ServerOAuth2AuthorizedClientExchangeFilterFunction so that it allows me to override the default token to change the principal name (and maybe ask this as an upgrade to spring?).
  2. I could use a different ReactiveOAuth2AuthorizedClientService per requested_subject. This would require me to create some kind of factory on top of it but nothing too big.
  3. I guess a 3rd option would be to inject a custom context in the ReactiveSecurityContextHolder from my ExchangeFilterFunction but I haven't tried to pursue this option because I'm really not sure how this is going to behave if multiple queries for different users are triggered in parallel.

Solution 2 seems the cleanest to me so far.

How can I properly authenticate with a different requested_subject only when needed and in a spring-friendly way ?


Solution

  • The third option is actually the intended way to override the authentication. It uses the reactive Context (not a ThreadLocal like in a servlet application).