spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.41k stars 40.51k forks source link

Broaden OAuth2 client auto-configuration to include non servlet web applications #40997

Open mcordeiro73 opened 3 months ago

mcordeiro73 commented 3 months ago

Attempting to setup a Spring Boot application that is not a Web Application, unable to rely on auto configuration of OAuth2Client due to OAuth2ClientAutoConfiguration having @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET).

The batch job we are attempting to setup has a client to a Rest API that requires OAuth2 authentication. When attempting to Autowire ClientRegistrationRepository and OAuth2AuthorizedClientService into a Bean method in order so setup the RestClient, received error that Parameter 1 of method oauth2RestClient in com.sample.batch.configuration.RestClientConfiguration required a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found.

We were able to get around this by providing our own ClientRegistrationRepository and OAuth2AuthorizedClientService beans, but I believe that these auto configured beans should not require the Spring Boot app to be running in a servlet environment.

wilkinsona commented 2 months ago

@mcordeiro73, can you please share an example of how you're setting up the RestClient?

@rwinch, do you think this makes sense for imperative non-web apps as #14350 did for reactive non-web apps?

mcordeiro73 commented 2 months ago

Below is the configuration of my RestClient. I've also added some custom OAuth classes I'm setting up to handle the OAuth token requests and removal of authorized client on token errors.

@Bean
public RestClient oktaRestClient(RestClient.Builder builder, ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService oauth2AuthorizedClientService) {
  OAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oauth2AuthorizedClientService);
  ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("okta");

  return builder.requestInterceptor(new Oauth2ClientHttpRequestInterceptor(authorizedClientManager, clientRegistration))
      .defaultStatusHandler(RemoveAuthorizedClientOAuth2ResponseErrorHandler.unauthorizedStatusPredicate(), RemoveAuthorizedClientOAuth2ResponseErrorHandler.createErrorHandler(oauth2AuthorizedClientService, clientRegistration))
      .build();
}
public class Oauth2ClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
    private final ClientRegistration clientRegistration;

    public Oauth2ClientHttpRequestInterceptor(@NotNull OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, @NotNull ClientRegistration clientRegistration) {
        this.oAuth2AuthorizedClientManager = oAuth2AuthorizedClientManager;
        this.clientRegistration = clientRegistration;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders()
                .setBearerAuth(getBearerToken());
        return execution.execute(request, body);
    }

    private String getBearerToken() {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(clientRegistration.getClientId())
                .build();

        OAuth2AuthorizedClient client = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest);
        Assert.notNull(client, () -> "Authorized client failed for Registration id: '" + clientRegistration.getRegistrationId() + "', returned client is null");
        return client.getAccessToken()
                .getTokenValue();
    }
}
public class RemoveAuthorizedClientOAuth2ResponseErrorHandler extends DefaultResponseErrorHandler {

    private static final Predicate<HttpStatusCode> STATUS_PREDICATE = httpStatusCode -> httpStatusCode.value() == HttpStatus.UNAUTHORIZED.value();

    private final OAuth2AuthorizedClientService oauth2AuthorizedClientService;
    private final ClientRegistration clientRegistration;

    public RemoveAuthorizedClientOAuth2ResponseErrorHandler(@NotNull OAuth2AuthorizedClientService oauth2AuthorizedClientService, @NotNull ClientRegistration clientRegistration) {
        this.oauth2AuthorizedClientService = oauth2AuthorizedClientService;
        this.clientRegistration = clientRegistration;
    }

    @Override
    public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
        if (STATUS_PREDICATE.test(clientHttpResponse.getStatusCode())) {
            oauth2AuthorizedClientService.removeAuthorizedClient(clientRegistration.getRegistrationId(), clientRegistration.getClientId());
        }
        super.handleError(clientHttpResponse);
    }

    public static RestClient.ResponseSpec.ErrorHandler createErrorHandler(@NotNull OAuth2AuthorizedClientService oauth2AuthorizedClientService, @NotNull ClientRegistration clientRegistration) {
        ResponseErrorHandler responseErrorHandler = new RemoveAuthorizedClientOAuth2ResponseErrorHandler(oauth2AuthorizedClientService, clientRegistration);
        return (request, response) -> responseErrorHandler.handleError(response);
    }

    public static Predicate<HttpStatusCode> unauthorizedStatusPredicate() {
        return STATUS_PREDICATE;
    }

}
rwinch commented 1 month ago

@wilkinsona This sounds like a useful change since clients are not necessarily web applications.

wilkinsona commented 1 month ago

Thanks, @rwinch.

When we come to implement this, we may want to review the package that OAuth2ClientAutoConfiguration is in. It's currently in org.springframework.boot.autoconfigure.security.oauth2.client.servlet which arguably doesn't make sense if it's going to apply to everything other than reactive web applications.