Spring Web Starter version: 2.4.12 Spring Web Version: 5.3.12

Issue: My application implements both a oauth2 resource server and oauth2 client and both use different authentication methods, but I can only get one working at a given time.

My inbound Controllers are protected by a Microsoft ADFS Token which is configured in a WebSecurityConfigurerAdapter that includes a jwt token.

I make an outbound call to another application that uses Pivotal Cloud Foundry UAA service client. When I make the call to that application with a Controller that is protected by my WebSecurityConfigurerAdaptor, it fails with this exception:

java.lang.IllegalArgumentException: principalName cannot be empty
    at org.springframework.util.Assert.hasText(Assert.java:289) ~[spring-core-5.3.12.jar:5.3.12]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ? Request to POST https://externalURL/v1/documents/upload [DefaultWebClient]
Stack trace:
        at org.springframework.util.Assert.hasText(Assert.java:289) ~[spring-core-5.3.12.jar:5.3.12]
        at org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService.loadAuthorizedClient(InMemoryOAuth2AuthorizedClientService.java:78) ~[spring-security-oauth2-client-5.4.9.jar:5.4.9]
        at org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository.loadAuthorizedClient(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java:80) ~[spring-security-oauth2-client-5.4.9.jar:5.4.9]
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:152) ~[spring-security-oauth2-client-5.4.9.jar:5.4.9]
        at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$24(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:552) ~[spring-security-oauth2-client-5.4.9.jar:5.4.9]
        at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:86) ~[reactor-core-3.4.11.jar:3.4.11]

I have found some outdated examples on how to tune my WebSecurityConfigurerAdapter (when not using jwt) to set a default principalName, but I think it is weird that my outbound calls are using configurations set up for my inbound calls.

Here is the WebClient code I am using

package com.ford.workflow.archive.config;

import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

import com.ford.workflow.archive.util.AdOAuth2ClientCredentialsGrantRequestEntityConverter;

import reactor.netty.http.client.HttpClient;

@Configuration
public class WebClientConfiguration {
    @Autowired
    ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;

    @Value("${external.endpoint}")
    String serviceUri;

    @Value("${web.client.response.timeout}")
    private long timeout;

    @Bean
    @Qualifier("document-upload-service-client")
    public WebClient customerServiceApicClient() {
        return WebClient.builder()
                .baseUrl(serviceUri).clientConnector(new ReactorClientHttpConnector(HttpClient.create().responseTimeout(Duration.ofSeconds(timeout))))
                .apply(oauth2Configuration("external-service-client", Optional.empty()))
                .build();
    }

    @Bean
    public WebClient getWebClient() {
        var httpClient = getHttpClient();
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

    private HttpClient getHttpClient() {
        return HttpClient.create()
                .responseTimeout(Duration.ofSeconds(5));
    }

    private Consumer<WebClient.Builder> oauth2Configuration(String clientRegistrationId, Optional<String> resource) {

        OAuth2AuthorizedClientManager oAuth2ClientManager = getOAuth2AuthorizedClientManager(resource);

        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(oAuth2ClientManager);
        oauth2.setDefaultClientRegistrationId(clientRegistrationId);

        return oauth2.oauth2Configuration();
    }

    public OAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager(Optional<String> resource) {
        DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
        clientCredentialsTokenResponseClient.setRequestEntityConverter(new AdOAuth2ClientCredentialsGrantRequestEntityConverter(resource));

        ClientCredentialsOAuth2AuthorizedClientProvider ccAuthorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
        ccAuthorizedClientProvider.setAccessTokenResponseClient(clientCredentialsTokenResponseClient);
        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, oAuth2AuthorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(ccAuthorizedClientProvider);

        return authorizedClientManager;
    }
}

Is there a different class or tuning for my WebClient that will not reuse components of my inbound WebSecurity?

Comment From: jgrandja

@lashower I suspect the issue is related to a misconfiguration. Take a look at this sample, which has a Resource Server that is configured as a Client as well.

See the Controller method, which receives the JwtAuthenticationToken and then makes an outbound call using WebClient.

If you're still having issues after looking at the sample, please provide a minimal reproducible sample so I can efficiently troubleshoot your issue.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: lashower

First, Thank you so much for your help. I worked with 2 architects and they had no clue on how to fix this. While my simple attempt to add in just the .oauth2Client(Customizer.withDefaults()) didn't resolve the issue, I am working on making my code align more to what you gave me.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.