In spring-security-oauth2-client 5.1, OAuth2 client is supported fairly well with webflux's WebClient via ServerOAuth2AuthorizedClientExchangeFilterFunction.
However, there is no equivalent support for webflux's WebSocketClient. For example, I would like to:
* obtain an access token from ClientRegistration / OAuth2AuthorizedClient
* automatically refresh the token before sending it if required, similar to how tokens are automatically refreshed in ServerOAuth2AuthorizedClientExchangeFilterFunction
* include the access token in the Authorization header of the initial websocket outbound upgrade request
In my application, I'm currently debating on whether I want to copy/paste ServerOAuth2AuthorizedClientExchangeFilterFunction and it's corresponding OAuth2AuthorizedClientResolver (which is package-private) in order to provide similar support for my websocket use cases.
It's really a shame that WebSocketClient does not use ExchangeFilterFunctions, otherwise we'd get this for free. Instead, it looks like I'll have to use reactor netty's HttpClient.headersWhen method as a hook to provide headers instead.
At a minimum, it would be nice if most of the logic for obtaining an access token in ServerOAuth2AuthorizedClientExchangeFilterFunction was extracted out into a class that could be reused in
* an ExchangeFilterFunction (for WebClient),
* a "headersWhen function" (for WebSocketClient).
* any other location where an access token is needed (e.g. a different http client or 3rd party sdk)
Mono<OAuth2AuthorizedClient> OAuth2AuthorizedClientResolver.loadAuthorizedClient is almost what I need. Except it doesn't handle refreshing tokens, and it's not public.
Comment From: philsttr
I'm not proud of this, but what I ended up doing as a workaround (rather than copy/paste ServerOAuth2AuthorizedClientExchangeFilterFunction) to capture the Authorization header value so that I can reuse it in other places (e.g. WebSocketClient) is:
- Create an
ExchangeFunctionthat has two filters that execute in the following order: ServerOAuth2AuthorizedClientExchangeFilterFunction- a custom
ExchangeFilterFunctionthat:- if the request is a bogus request (from step 2) capture the request's
Authorizationheader and returns aClientResponsewith anAuthorizationheader (without invoking the downstreamExchangeFunction) - else invoke the downstream
ExchangeFunction(to handle requests created by theServerOAuth2AuthorizedClientExchangeFilterFunction, such as a request to refresh the token)
- if the request is a bogus request (from step 2) capture the request's
- Send a bogus request through the
ExchangeFunctioncreated in step 1 - grab the
Authorizationheader from theClientResponse
Using this stream, I can reuse ExchangeFilterFunctions provided by spring security to generically obtain the Authorization header value for use in places other than a WebClient.
Even though I have a workaround, I'd still rather spring-security-oauth2-client provide a generic mechanism that can be used to retrieve tokens that can be used in any outbound client.
Comment From: jgrandja
@philsttr Thanks for the report. This is the first request for oauth2 client support using WebSocketClient. I haven't seen any demand for this yet and I don't recall seeing any use cases for it either? We are quite stretched with our current plan for 5.2 and still need to build out further support in WebClient.
I will see what I can do to extract logic out so it can potentially be reused in WebSocketClient.
Let's leave this issue open and see if other users are looking for this type of integration.
Comment From: etienne-sf
Hello,
@philsttr ,
I start with a big "Thank you" to you! I had exactly this need. And your workaround works :) . I searched for hours, and days, until finding this thread. For those interested, you'll find an implementation in this class: https://github.com/graphql-java-generator/graphql-maven-plugin-project/blob/generate_relay_schema/graphql-java-runtime/src/main/java/com/graphql_java_generator/client/OAuthTokenExtractor.java It is used in this class: https://github.com/graphql-java-generator/graphql-maven-plugin-project/blob/generate_relay_schema/graphql-java-runtime/src/main/java/com/graphql_java_generator/client/QueryExecutorSpringReactiveImpl.java (in the second execute method)
@jgrandja,
My use case is this one: I'm building a java code generator, to generate GraphQL client or server: https://github.com/graphql-java-generator/graphql-maven-plugin-project A GraphQL server allows operations that can be: query, mutation or subscription. A subscription is actually a Websocket. So, to access a GraphQL server that has subscription operations, and that is protected by OAuth2 (which seems quite the state of the art), we need a way to open a WebSocket with an OAuth authorization. Now, I have a workaround.
But perhaps this message could help to make this request be implemented ?
Etienne
Comment From: jgrandja
@etienne-sf I'm still not seeing much demand for OAuth2 WebSocketClient support. There are quite a few higher priority items that we are working on so this will remain in the Icebox. Upvotes help with prioritizing features.
Comment From: etienne-sf
Yes, I understand. Actually, I expected this answer.
BTW, thanks to phillsttr, I have a working workaround. And hopefully, it can be useful for others too.
Étienne
Comment From: jgrandja
@philsttr I'm going to close this as there isn't much demand for OAuth2 WebSocketClient support.
If there are areas in the code that we could re-factor to allow for greater reuse and make it easier for you (and others) to work with WebSocketClient then we would be open to these suggestions.
Comment From: VitaliyKulikov
Hey, I demand it )). Reopen plz!
Comment From: maikwodarz
Precondition is my client id registration with registration id: johndoeservice
my properties:
spring:
security:
oauth2:
client:
registration:
johndoeservice:
client-id: f72351d3-bab3-42cd-a261-c6clientId6f
client-secret: UAy8Q~vignl7SKQQbigsecretJHzZHZN-kaEP
authorization-grant-type: client_credentials
scope: api://f72351d3-bab3-42cd-a261-c6clientId6f/.default
provider:
johndoeservice:
authorization-uri: https://login.myauthprovider.com/334553-2332323-23232/oauth2/v2.0/authorize
token-uri: https://login.myauthprovider.com/334553-2332323-23232/oauth2/v2.0/token
Getting JWT with Spring without filter:
OAuth2AuthorizeRequest req = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId).principal(principal).build();
OAuth2AuthorizedClient authorizedClient oAuth2AuthorizedClientManager.authorize(req);
OAuth2AccessToken newAccessToken = authorizedClient.getAccessToken();
String jwt = newAccessToken.getTokenValue();
I have the whole thing cached and also picked the expiration date from jwt. If a service wants the token from the cache shortly before it expires, the chache takes a new one.
During websocket connect:
WebSocketHttpHeaders httpHeaders = new WebSocketHttpHeaders();
httpHeaders.add(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);
CompletableFuture<StompSession> completableFuture = webSocketStompClient.connectAsync(settings.getEndpoint(), httpHeaders, new StompHeaders(), sessionHandler);
As result, you have the Auth Header in WebSocket connect. You can observe it with Wireshark. When SpringBoot receives this connect with invalid Header it denies. Otherwise it lets work.
I tested it with SpringBoot 3.2.7 until 3.4.1
Drawback is that you have to care about expiration. Without caching you would call auth endpoint on every request.