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:

  1. Create an ExchangeFunction that has two filters that execute in the following order:
  2. ServerOAuth2AuthorizedClientExchangeFilterFunction
  3. a custom ExchangeFilterFunction that:
    • if the request is a bogus request (from step 2) capture the request's Authorization header and returns a ClientResponse with an Authorization header (without invoking the downstream ExchangeFunction)
    • else invoke the downstream ExchangeFunction (to handle requests created by the ServerOAuth2AuthorizedClientExchangeFilterFunction, such as a request to refresh the token)
  4. Send a bogus request through the ExchangeFunction created in step 1
  5. grab the Authorization header from the ClientResponse

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.