Describe the bug Google Access Token (Not JWT) is sent as a Bearer header while calling a resource server using Web Client. At resource server a valid JWT is expected but access token can not be parsed as JWT and 401 is returned.

http://localhost:8080/google endpoint calls http://localhost:8081/secured-google

When we call http://localhost:8080/google endpoint we get authenticated by google and receive 500 http error. In logs we can see

org.springframework.web.reactive.function.client.WebClientResponseException$Unauthorized: 401 Unauthorized from GET http://localhost:8081/secured-google
    at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:181) ~[spring-webflux-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ 401 from GET http://localhost:8081/secured-google [DefaultWebClient]
Stack trace:
        at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:181) ~[spring-webflux-5.2.8.RELEASE.jar:5.2.8.RELEASE]
        at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:206) ~[spring-webflux-5.2.8.RELEASE.jar:5.2.8.RELEASE]
        at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:100) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
        at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:100) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:144) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
...

Problem is in class ServletOAuth2AuthorizedClientExchangeFilterFunction where access token is added a Authorization header.

    private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) {
        return ClientRequest.from(request)
                    .headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
                    .attributes(oauth2AuthorizedClient(authorizedClient))
                    .build();
    }

To Reproduce

Create a Spring Boot Client Application 1. Create a client spring boot application with oauth2 login security configuration.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().anyRequest().authenticated()
            .and().oauth2Login() ;
    }
}
  1. Create a custom WebClient bean to use ServletOAuth2AuthorizedClientExchangeFilterFunction
@Configuration
public class WebClientConfig {
    @Bean
    protected WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository auth2AuthorizedClientRepository) {

        ServletOAuth2AuthorizedClientExchangeFilterFunction oauthFilterFunction = 
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, auth2AuthorizedClientRepository) ;

        return WebClient.builder().apply(oauthFilterFunction.oauth2Configuration()).build();
    }
}
  1. Create a controller to use WebClient in order to call a resource server protected via JWT.
@RestController
public class GoogleController {
    @Autowired
    private WebClient webClient ;
    @GetMapping("/google")
    public String google(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient,
            @AuthenticationPrincipal OAuth2User oauth2User) {

        return this.webClient
                .get()
                .uri("http://localhost:8081/secured-google")
                .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient))
                .retrieve()
                .bodyToMono(String.class)
                .block() ;
    }
}
  1. Configure application.yml for using google oauth
server.port: 8080
spring:
  security:
    oauth2:
      client:
        registration:
          google:   
            client-id: <CLIENT-ID>
            client-secret: <CLIENT-SECRET>

Client a Spring Boot Resource Server 1. Create another spring boot resource sever application with following security config

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().anyRequest().authenticated()
            .and().oauth2ResourceServer().jwt();
    }
}
  1. Create a controller to be authenticated via JWT
@RestController
public class SecuredGoogleController {
    @GetMapping("/secured-google")
    public String secured(JwtAuthenticationToken token) {
        return "secured id " + token.getName() ;
    }
}
  1. Configure application.yml for using google oauth
server.port: 8081
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://accounts.google.com
          jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs

Expected behavior OidcUser IdToken (which is JWT) must be sent instead of Access Token.

Github repository You can find minimal and reproducible sample at github repo https://github.com/rdavudov/spring-security.

Workaround Manually provide Id Token using web client as a header.

    @GetMapping("/google-ok")
    public String googleOk(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient,
            @AuthenticationPrincipal OAuth2User oauth2User) {
        String idToken = ((OidcUser) oauth2User).getIdToken().getTokenValue() ;

        return this.webClient
                .get()
                .uri("http://localhost:8081/secured-google")
                .headers(header -> header.setBearerAuth(idToken))
                .retrieve()
                .bodyToMono(String.class)
                .block() ;
    }

Comment From: jgrandja

@rdavudov

OidcUser IdToken (which is JWT) must be sent instead of Access Token.

This is not correct. The ID Token is used by the client to authenticate the user. See reference

OpenID Connect performs authentication to log in the End-User or to determine that the End-User is already logged in. OpenID Connect returns the result of the Authentication performed by the Server to the Client in a secure manner so that the Client can rely on it. For this reason, the Client is called Relying Party (RP) in this case.

The ID Token is usually not passed to a resource server, however, I have seen a couple of cases where it has. The access token is meant to be passed to the resource server, which can either be an opaque token or self-contained token (JWT). See reference.

Google access tokens are opaque by default and only Google resource servers can parse and authorize them, e.g. Calendar API, Gmail API, etc.

You have configured a custom resource server with JWT support and passing it an opaque token from Google. This is a misconfiguration. You could configure oauth2ResourceServer().opaqueToken() instead that calls the Google introspection endpoint to validate the opaque token. However, I'm not sure if this makes sense either. The Google access token would typically be passed to a Google resource server. See opaque token reference.

I'm going to close this based on application misconfiguration.