Spring Security 5 Calling OAuth2 Secured API in Application Runner results in IllegalArgumentException
Asked Answered
E

2

19

Given the following code, is it possible to call a Client Credentials secured API in an application runner?

@Bean
public ApplicationRunner test(
    WebClient.Builder builder,
    ClientRegistrationRepository clientRegistrationRepo, 
    OAuth2AuthorizedClientRepository authorizedClient) {
        return args -> {
            try {
                var oauth2 =
                    new ServletOAuth2AuthorizedClientExchangeFilterFunction(
                        clientRegistrationRepo,
                        authorizedClient);
                oauth2.setDefaultClientRegistrationId("test");
                var response = builder
                    .apply(oauth2.oauth2Configuration())
                    .build()
                    .get()
                    .uri("test")
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();
                log.info("Response - {}", response);
            } catch (Exception e) {
                log.error("Failed to call test.", e);
            }
        };
    }

The code fails due to,

java.lang.IllegalArgumentException: request cannot be null

Full stack,

java.lang.IllegalArgumentException: request cannot be null
    at org.springframework.util.Assert.notNull(Assert.java:198) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
    at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.loadAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:47) ~[spring-security-oauth2-client-5.1.4.RELEASE.jar:5.1.4.RELEASE]
    at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.populateDefaultOAuth2AuthorizedClient(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:364) ~[spring-security-oauth2-client-5.1.4.RELEASE.jar:5.1.4.RELEASE]
    at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$null$2(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:209) ~[spring-security-oauth2-client-5.1.4.RELEASE.jar:5.1.4.RELEASE]
    at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.attributes(DefaultWebClient.java:234) ~[spring-webflux-5.1.5.RELEASE.jar:5.1.5.RELEASE]
    at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.attributes(DefaultWebClient.java:153) ~[spring-webflux-5.1.5.RELEASE.jar:5.1.5.RELEASE]

With the failing method looking like,

public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(
    String clientRegistrationId,  Authentication principal, HttpServletRequest request){

    Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
    Assert.notNull(request, "request cannot be null");
    return (OAuth2AuthorizedClient)this
        .getAuthorizedClients(request)
        .get(clientRegistrationId);
}

Which makes sense as there is not HttpServletRequest for it to use, its being called on start-up of the application.

Is there any workarounds other than make my own no-op OAuth2AuthorizedClientRepository?

//Edit,

This is not a fully reactive stack. It is a Spring Web stack with the WebClient being used with in it.

I am well aware of the ServerOAuth2AuthorizedClientExchangeFilterFunction which applies to a fully reactive stack and requires ReactiveClientRegistrationRepository and ReactiveOauth2AuthorizedClient which are not available due to this being in an application built on top of Servlet stack, not reactive.

Erysipeloid answered 22/3, 2019 at 23:10 Comment(2)
what is your use case? can you provide more contextPearce
The "servletRequest cannot be null" was seems to be a result from not being in a Servlet Context. Using WebClient inside Controller or RestController works fine, but from a Component or Service you will get the exception. Scorpioo's answer below works great.Hackney
S
34

Since I also stumbled across this problem I'll elaborate a bit on Darren Forsythe's updated answer to make it easier for others to find:

The issue submitted by the OP resulted in an implementation of OAuth2AuthorizedClientManager that is capable of

operating outside of a HttpServletRequest context, e.g. in a scheduled/background thread and/or in the service-tier

(from the official docs)

Said implementation, the AuthorizedClientServiceOAuth2AuthorizedClientManager, is passed to the ServletOAuth2AuthorizedClientExchangeFilterFunction to replace the default one.

In my example this looks something like this:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientService clientService)
{

    OAuth2AuthorizedClientProvider authorizedClientProvider = 
        OAuth2AuthorizedClientProviderBuilder.builder()
            .clientCredentials()
            .build();

    AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = 
        new AuthorizedClientServiceOAuth2AuthorizedClientManager(
            clientRegistrationRepository, clientService);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager)
{
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
        new ServletOAuth2AuthorizedClientExchangeFilterFunction(
            authorizedClientManager);
    oauth2.setDefaultClientRegistrationId("keycloak");
    return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}
Sweven answered 12/1, 2020 at 18:32 Comment(7)
This works for me. So whenever you want to use @Scheduled on service which uses OAuth2 to call another rest service, you need to have custom OAuth2AuthorizedClientManager.Obscene
This solution was kinda hard to spot for me at first. It looks much like the Spring Security Oauth2 Migration example but there is two important a differences. Note the OAuth2AuthorizedClientService clientService in the first bean and creation of AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager in the second bean. Thank you Scorpioo590 this solved my overdue problem!Hackney
What spring security version is this? There's no such class OAuth2AuthorizedClientManager , i'm using 5.1.5Schwaben
I am using the Spring Security Starter 2.2.2 which uses 5.2.1 of Spring SecuritySweven
You saved my time sir, thank you for providing the solution :)Contrast
It is also an option to use the WebClient.Builder.defaultRequest(req -> req.attributes(clientRegistrationId("keycloak"))) instead of oauth2.setDefaultClientRegistrationId("keycloak"); As described here: github.com/spring-projects/spring-framework/issues/…Unteach
We have an integration test for our WebClient, but test is not failing although there is no servlet context. What is missing?Makassar
E
3

I ended up asking this to the Spring Security team,

https://github.com/spring-projects/spring-security/issues/6683

Unfortunately if you are on the servlet stack and calling to OAuth2 resources with pure Spring Security 5 APIs in a background thread there isn't a OAuth2AuthorizedClientRepository available.

Realistically there's two options,

  1. Implement a completely no-op version,
 var oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo,
                        new OAuth2AuthorizedClientRepository() {
                            @Override
                            public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String s,
                                    Authentication authentication, HttpServletRequest httpServletRequest) {
                                return null;
                            }

                            @Override
                            public void saveAuthorizedClient(OAuth2AuthorizedClient oAuth2AuthorizedClient,
                                    Authentication authentication, HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse) {

                            }

                            @Override
                            public void removeAuthorizedClient(String s, Authentication authentication,
                                    HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {

                            }
                        });
  1. Implement a Servlet Version of UnAuthenticatedServerOAuth2AuthorizedClientRepository. UnAuthenticatedServerOAuth2AuthorizedClientRepository GitHub Source which has some basic functionality than a pure no-op.

Providing feedback on the GitHub issue might help the Spring Security team to evaluate accepting a PR and maintaining a Servlet version of the UnAuthenticatedServerOAuth2AuthorizedClientRepository

I reached out the the Spring Security Team Spring Security Issue 6683 and on the back of that a Servlet version of the ServerOAuth2AuthorizedClientExchangeFilterFunction will be added in Spring Security 5.2 for usage on non-http threads.

Erysipeloid answered 1/4, 2019 at 12:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.