Describe the bug
In some configuration setups, adding a ServletOAuth2AuthorizedClientExchangeFilterFunction
to a WebClient
that can retry on authorization failures causes an IllegalArgumentException
when trying to remove the authorized client in cases where the WebClient
receives a response actionable by the authorization failure forwarder. The ClientRequest
is not always populated with a HttpServletRequest
attribute when the filter is added via the WebClientBuilder.filter()
method instead of using the the filter's filter.oauth2Configuration()
method.
The filter.filter()
method does merge the required objects into the ClientRequest
when making an authorized client, but then does not use that merged request when executing the handler within the filter. Without the merged attributes, its impossible for the AuthorizationFailureForwarder
to clean up the client in case there is a 401/403 response from the original ClientRequest
.
To Reproduce
- Execute sample repository according to the
readme.md
Expected behavior
It is expected that the authorizedClient is removed via the AuthorizationFailureForwarder
in the case a webclient with a ServletOAuth2AuthorizedClientExchangeFilterFunction
.
Sample
Comment From: jjstreet
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
// @formatter:off
return mergeRequestAttributesIfNecessary(request)
.filter((req) -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent())
.flatMap((req) -> reauthorizeClient(getOAuth2AuthorizedClient(req.attributes()), req))
.switchIfEmpty(
Mono.defer(() ->
mergeRequestAttributesIfNecessary(request)
.filter((req) -> resolveClientRegistrationId(req) != null)
.flatMap((req) -> authorizeClient(resolveClientRegistrationId(req), req))
)
)
.map((authorizedClient) -> bearer(request, authorizedClient))
.flatMap((requestWithBearer) -> exchangeAndHandleResponse(requestWithBearer, next))
.switchIfEmpty(Mono.defer(() -> exchangeAndHandleResponse(request, next)));
// @formatter:on
}
The problem is the ClientRequest
that is created by mergeRequestAttributesIfNecessary()
is not passed to exchangeAndHandleResponse
and thus, situations where the client must be cleaned up fail with an IllegalArgumentException
Comment From: jjstreet
By adjusting the filter
methods as shown, the merged request is used by the exchangeAndHandleResponse
methods. Then the client can be cleaned up properly.
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return mergeRequestAttributesIfNecessary(request)
.flatMap(merged -> Mono.just(merged)
.filter((req) -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent())
.flatMap((req) -> reauthorizeClient(getOAuth2AuthorizedClient(req.attributes()), req))
.switchIfEmpty(
Mono.just(merged)
.filter((req) -> resolveClientRegistrationId(req) != null)
.flatMap((req) -> authorizeClient(resolveClientRegistrationId(req), req)))
.map((authorizedClient) -> bearer(merged, authorizedClient))
.flatMap((requestWithBearer) -> exchangeAndHandleResponse(requestWithBearer, next))
.switchIfEmpty(exchangeAndHandleResponse(merged, next)));
}
As you can see, I flatMap
on the merging so that I can use it in rest of the pipeline, which includes the the points where the exchange is handled.
Comment From: jjstreet
The mergeRequestAttributesIfNecessary()
is what is confusing here, because if we use the convenience method to initially set up the filter on the webClient, it adds the the request/response objects to the client request attributes. There is no reason to do so again?
With the proposed fix, it might not be necessary to have the convenience method filter.oauth2Configuration()
. It seems to be the only oauth2 filter function that has it, as the reactive variants do not. But I also do not know if there is any problem with the reactive variants either (though i doubt it, given there are no servlet requests objects to attach).
Comment From: jjstreet
I've created a sample repository that illustrates the issue.