How to configure Spring WebClient to reuse access tokens

25 Views Asked by At

I'm trying to use the spring WebClient to access some OAuth2 secured REST services. It works but it seems that it does not reuse the access token between requests. I started with password grant and refresh token and the refresh token was used to get a new access token with every request. Then I tried to use client credentials, since it seemed that not everything is supported with password grant type - but the behavior was the same, it requested a new access token on every request also the token is valid for 5 minutes. To make sure it is not some strange configuration in my application or an old bug, I created a new empty spring boot app with the current version 3.2.4 and the minimal dependencies:

  • org.springframework.boot:spring-boot-starter-oauth2-client
  • org.springframework.boot:spring-boot-starter-web
  • org.springframework:spring-webflux
  • org.springframework.boot:spring-boot-starter-test

I create the WebClient like that:

  @Bean
  WebClient webClient(
          WebClient.Builder builder,
          ClientRegistrationRepository clientRegistrationRepository,
          OAuth2AuthorizedClientRepository authorizedClientRepository ) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction( clientRegistrationRepository, authorizedClientRepository );
    oauth.setDefaultClientRegistrationId( "khtest" );
    return builder.filter( oauth ).build();
  }

Then I use the in injected WebClient like this:

    String result = webClient.get()
            .uri( "http://172.28.80.1:8080/auth/admin/realms/AI/groups" )
            .retrieve()
            .bodyToMono( String.class )
            .block();

If I call this two times after another, I get the following logs:

start first REST call
2024-03-28T10:32:18.716+01:00 DEBUG 50808 --- [demo] [    Test worker] o.s.http.codec.FormHttpMessageWriter     : [97b84a4] Writing {grant_type=[client_credentials]}
2024-03-28T10:32:18.734+01:00 DEBUG 50808 --- [demo] [    Test worker] o.s.w.r.f.client.ExchangeFunctions       : [97b84a4] HTTP POST http://172.28.80.1:8080/auth/realms/AI/protocol/openid-connect/token
2024-03-28T10:32:18.802+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [97b84a4] [6f5d4358] Response 200 OK
2024-03-28T10:32:18.840+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.http.codec.json.Jackson2JsonDecoder  : [97b84a4] [6f5d4358] Decoded [{access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2RGdhSXNRR0REVm44Tk9USlk2cElSRnNHMH (truncated)...]
2024-03-28T10:32:18.848+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [97b84a4] Cancel signal (to close connection)
2024-03-28T10:32:18.852+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.http.codec.FormHttpMessageWriter     : [1cc1a157] Writing {grant_type=[client_credentials]}
2024-03-28T10:32:18.853+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [1cc1a157] HTTP POST http://172.28.80.1:8080/auth/realms/AI/protocol/openid-connect/token
2024-03-28T10:32:18.889+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [1cc1a157] [1daa2410] Response 200 OK
2024-03-28T10:32:18.891+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.http.codec.json.Jackson2JsonDecoder  : [1cc1a157] [1daa2410] Decoded [{access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2RGdhSXNRR0REVm44Tk9USlk2cElSRnNHMH (truncated)...]
2024-03-28T10:32:18.891+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [1cc1a157] Cancel signal (to close connection)
2024-03-28T10:32:18.892+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [74a74070] HTTP GET http://172.28.80.1:8080/auth/admin/realms/AI/groups
2024-03-28T10:32:18.899+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [74a74070] [7085d850] Response 200 OK
2024-03-28T10:32:18.901+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] org.springframework.web.HttpLogging      : [74a74070] [7085d850] Decoded "[{"id":"c129933b-0270-43ac-85bf-050f7d7ca35a","name":"ai-keycloak-admin","path":"/ai-keycloak-admin" (truncated)..."
[{"id":"c129933b-0270-43ac-85bf-050f7d7ca35a","name":"ai-keycloak-admin","path":"/ai-keycloak-admin","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}},{"id":"c5d34304-95a0-4a33-808b-9224c207eeed","name":"business","path":"/business","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}},{"id":"c70e3556-e620-4a9e-86d3-220540930669","name":"business-readonly","path":"/business-readonly","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}},{"id":"6af3b23a-706c-4d42-9c80-37648e186c5e","name":"management","path":"/management","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}}]
finished first REST call
start second REST call immediately, access token should still be valid
2024-03-28T10:32:18.906+01:00 DEBUG 50808 --- [demo] [    Test worker] o.s.http.codec.FormHttpMessageWriter     : [12bcf7c6] Writing {grant_type=[client_credentials]}
2024-03-28T10:32:18.906+01:00 DEBUG 50808 --- [demo] [    Test worker] o.s.w.r.f.client.ExchangeFunctions       : [12bcf7c6] HTTP POST http://172.28.80.1:8080/auth/realms/AI/protocol/openid-connect/token
2024-03-28T10:32:18.918+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [12bcf7c6] [2a2aae5b] Response 200 OK
2024-03-28T10:32:18.919+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.http.codec.json.Jackson2JsonDecoder  : [12bcf7c6] [2a2aae5b] Decoded [{access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2RGdhSXNRR0REVm44Tk9USlk2cElSRnNHMH (truncated)...]
2024-03-28T10:32:18.919+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [12bcf7c6] Cancel signal (to close connection)
2024-03-28T10:32:18.922+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.http.codec.FormHttpMessageWriter     : [6b46d949] Writing {grant_type=[client_credentials]}
2024-03-28T10:32:18.923+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [6b46d949] HTTP POST http://172.28.80.1:8080/auth/realms/AI/protocol/openid-connect/token
2024-03-28T10:32:18.939+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [6b46d949] [451c14d8] Response 200 OK
2024-03-28T10:32:18.940+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.http.codec.json.Jackson2JsonDecoder  : [6b46d949] [451c14d8] Decoded [{access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2RGdhSXNRR0REVm44Tk9USlk2cElSRnNHMH (truncated)...]
2024-03-28T10:32:18.941+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [6b46d949] Cancel signal (to close connection)
2024-03-28T10:32:18.941+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [3a116ca6] HTTP GET http://172.28.80.1:8080/auth/admin/realms/AI/groups
2024-03-28T10:32:18.949+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] o.s.w.r.f.client.ExchangeFunctions       : [3a116ca6] [2131e65a] Response 200 OK
2024-03-28T10:32:18.950+01:00 DEBUG 50808 --- [demo] [onPool-worker-1] org.springframework.web.HttpLogging      : [3a116ca6] [2131e65a] Decoded "[{"id":"c129933b-0270-43ac-85bf-050f7d7ca35a","name":"ai-keycloak-admin","path":"/ai-keycloak-admin" (truncated)..."
[{"id":"c129933b-0270-43ac-85bf-050f7d7ca35a","name":"ai-keycloak-admin","path":"/ai-keycloak-admin","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}},{"id":"c5d34304-95a0-4a33-808b-9224c207eeed","name":"business","path":"/business","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}},{"id":"c70e3556-e620-4a9e-86d3-220540930669","name":"business-readonly","path":"/business-readonly","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}},{"id":"6af3b23a-706c-4d42-9c80-37648e186c5e","name":"management","path":"/management","subGroupCount":0,"subGroups":[],"access":{"view":true,"viewMembers":true,"manageMembers":true,"manage":true,"manageMembership":true}}]
finished second REST call

It looks like the application somehow makes not only one but two token requests for each REST call.

Looking at ServletOAuth2AuthorizedClientExchangeFilterFunction.filter, I don't really get how it should work. It checks for an entry for OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME in the requests attribute map but it seems that it is never filled, so it always calls authorizeClient but even if it was filled it would call reauthorizeClient, what looks like it will also result in a token request. I would have expected some check if the access token in the OAuth2AuthorizedClient was still valid, but I haven't found something like that. I also looked at the reactive variant (ServerOAuth2AuthorizedClientExchangeFilterFunction), but I got the same behavior there - and I'm not totally sure using the reactive version in a servlet application is a good idea.

I feel like I'm missing something here, any help is appreciated, thank you.

0

There are 0 best solutions below