Summary

I am writing a reactive Spring application that processes Google Pub/Sub messages with Spring Cloud Stream. For each message Auth0's management API should be called with a OAuth 2.0 client credentials token. However, the WebClient won't perform an authorization.

Actual Behavior

I don't have spring-boot-starter-security in my project. This is a "standalone" application that processes messages and calls for each message Auth0's management API. I have created a WebClient that should automatically use a OAuth 2.0 client credentials token. Unfortunately, the WebClient never performs an authorization. It just performs the call to the call to the management API without getting a token beforehand.

This is the error message: 2020-02-13 12:25:46.336 ERROR 5898 --- [ctor-http-nio-3] i.v.outputservice.auth0.Auth0Service : illegal response! status code: 401 UNAUTHORIZED, body: {"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}

Expected Behavior

WebClient should get a client credentials

Configuration

My Spring dependencies in build.gradle:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    // ...
    implementation 'org.springframework.security:spring-security-oauth2-client'

This is the @Configuration class:

@Configuration
@RequiredArgsConstructor
class Auth0Configuration {

    private static final String AUTH_0_CLIENT_REGISTRATION_ID = "auth0";

    @Value("${auth0.api.base-url}")
    private final String auth0ApiBaseUrl;

    @Value("${auth0.token-uri}")
    private final String auth0TokenUri;

    @Value("${auth0.client-id}")
    private final String auth0ClientId;

    @Value("${auth0.client-secret}")
    private final String auth0ClientSecret;

    @Bean
    WebClient auth0WebClient() {
        return WebClient.builder()
                .filter(createServerOAuth2AuthorizedClientExchangeFilterFunction(createClientRegistration()))
                .baseUrl(auth0ApiBaseUrl)
                .clientConnector(new ReactorClientHttpConnector(HttpClient.create().wiretap(true)))
                .build();
    }

    private static ServerOAuth2AuthorizedClientExchangeFilterFunction createServerOAuth2AuthorizedClientExchangeFilterFunction(ClientRegistration clientRegistration) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(
                        new DefaultReactiveOAuth2AuthorizedClientManager(
                                new InMemoryReactiveClientRegistrationRepository(clientRegistration),
                                new UnAuthenticatedServerOAuth2AuthorizedClientRepository()
                        ));
        serverOAuth2AuthorizedClientExchangeFilterFunction.setDefaultClientRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID);
        return serverOAuth2AuthorizedClientExchangeFilterFunction;
    }

    private ClientRegistration createClientRegistration() {
        return ClientRegistration.withRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .tokenUri(auth0TokenUri)
                .clientId(auth0ClientId)
                .clientSecret(auth0ClientSecret)
                .build();
    }

}

Version

Spring versions:

    id 'org.springframework.boot' version '2.2.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'

Java 13

Sample

    @Qualifier("auth0WebClient")
    private final WebClient webClient;

    // see https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id
    public Mono<User> getUser(String id) {
        return webClient.get()
                .uri(uriBuilder ->
                        uriBuilder.path("/api/v2/users/{id}")
                                .queryParam("fields", "email,email_verified")
                                .queryParam("include_fields", true)
                                .build(id))
                .exchange()
                .handle((BiConsumer<ClientResponse, SynchronousSink<ClientResponse>>) (clientResponse, synchronousSink) -> {
                    HttpStatus httpStatus = clientResponse.statusCode();
                    if (httpStatus.is2xxSuccessful()) {
                        synchronousSink.next(clientResponse);
                    } else {
                        synchronousSink.error(new IllegalResponseException(clientResponse));
                    }
                })
                .doOnError(IllegalResponseException.class, e -> {
                    ClientResponse clientResponse = e.clientResponse;
                    clientResponse.bodyToMono(String.class).subscribe(body ->
                            log.error("illegal response! status code: {}, body: {}", clientResponse.statusCode(), body));
                })
                .doOnNext(clientResponse -> log.debug("status code: {}", clientResponse.statusCode()))
                .flatMap((Function<ClientResponse, Mono<User>>) clientResponse -> clientResponse.bodyToMono(User.class));
    }

Comment From: ghost

The application is working now with this configuration:

@Configuration
@RequiredArgsConstructor
@Slf4j
class Auth0Configuration {

    // some properties

    @Bean
    WebClient auth0WebClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        var exchangeFilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        exchangeFilterFunction.setDefaultClientRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID);
        return WebClient.builder()
                .baseUrl(auth0ApiBaseUrl)
                .filter(exchangeFilterFunction)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }

    @Bean
    public ReactiveClientRegistrationRepository reactiveClientRegistrationRepository() {
        return new InMemoryReactiveClientRegistrationRepository(
                ClientRegistration.withRegistrationId(AUTH_0_CLIENT_REGISTRATION_ID)
                        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                        .tokenUri(auth0TokenUri)
                        .clientAuthenticationMethod(ClientAuthenticationMethod.POST)
                        .clientId(auth0ClientId)
                        .clientSecret(auth0ClientSecret)
                        .scope()
                        .build()
        );
    }

    @Bean
    public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        authorizedClientManager.setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials(clientCredentialsGrantBuilder -> {
                            WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
                            accessTokenResponseClient.setWebClient(WebClient.builder()
                                    .filter((request, next) -> {
                                        FormInserter<String> body = (FormInserter<String>) request.body();
                                        body.with("audience", auth0Audience);
                                        return next.exchange(request);
                                    })
                                    .build());
                            clientCredentialsGrantBuilder.accessTokenResponseClient(accessTokenResponseClient);
                        }
                )
                .build()
        );
        return authorizedClientManager;
    }

}