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.
Comment From: wilkinsona
@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?
Comment From: mcordeiro73
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;
}
}
Comment From: rwinch
@wilkinsona This sounds like a useful change since clients are not necessarily web applications.
Comment From: wilkinsona
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.
Comment From: swargolet
+1 to this request.
We just faced this issue today in our batch app. While it was simple enough to copy the InMemoryClientRegistrationRepository bean from OAuth2ClientRegistrationRepositoryConfiguration into our own config, that brought up another issue. ReactiveOAuth2ClientAutoConfiguration has @Conditional(ReactiveOAuth2ClientAutoConfiguration.NonServletApplicationCondition.class) among a few other reactive conditions. Since Spring Security Config itself brings in all the classes that are used as conditions for this autoconfiguration class and because our batch app is set to a web-application-type of none, it satisfies all conditions and thus creates these reactive beans.
As part of this enhancement, it would be nice if ReactiveOAuth2ClientAutoConfiguration was looking specifically for a reactive web app as opposed to a non-servlet web app.
Comment From: wilkinsona
As part of this enhancement, it would be nice if ReactiveOAuth2ClientAutoConfiguration was looking specifically for a reactive web app as opposed to a non-servlet web app.
It's looking for a non-servlet app, that is to say a reactive web appplication or a non-web application. I'm afraid we can't change that as it would break anyone who's using a reactive client in a non-web application.
Comment From: neoludo
So what is the best way to use OIDC client in a batch ?
Comment From: wilkinsona
@neoludo please read the whole issue if you have not already done so. @mcordeiro73 has kindly already described how they're configuring things manually in the auto-configuration's absence. If you have specific questions about how to configure things manually, please ask them on Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.
Comment From: neoludo
I did. So there is no plan to have an official starter working for non web apps ?
Le jeu. 10 oct. 2024, 11:52, Andy Wilkinson @.***> a écrit :
@neoludo https://github.com/neoludo please read the whole issue if you have not already done so. @mcordeiro73 https://github.com/mcordeiro73 has kindly already described how they're configuring things manually in the auto-configuration's absence. If you have specific questions about how to configure things manually, please ask them on Stack Overflow. As mentioned in the guidelines for contributing https://github.com/spring-projects/spring-boot/blob/main/CONTRIBUTING.adoc#using-github-issues, we prefer to use GitHub issues only for bugs and enhancements.
— Reply to this email directly, view it on GitHub https://github.com/spring-projects/spring-boot/issues/40997#issuecomment-2404625698, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAD76MKELJNSPZEWSEZKBEDZ2ZE43AVCNFSM6AAAAABI27UH7KVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIMBUGYZDKNRZHA . You are receiving this because you were mentioned.Message ID: @.***>
Comment From: wilkinsona
Why do you assume there's no plan when this issue, that remains open, is tracking the improvement that you seem to want? We hope to implement it in a future Spring Boot 3.x release. In the meantime, "the best way to use OIDC client" in a non-web app is described above.
Comment From: neoludo
Glad to hear that. I just had done a bad interpretation of your last comment last month. Sorry for that.
Comment From: mhalbritter
I looked at this, and we have the following situation at the moment:
- Servlet applications
- ClientRegistrationRepository: yes
- OAuth2AuthorizedClientService: yes
- OAuth2AuthorizedClientRepository: yes
- ReactiveClientRegistrationRepository: no
- ReactiveOAuth2AuthorizedClientService: no
- ServerOAuth2AuthorizedClientRepository: no
- Servlet applications with reactor-core
- ClientRegistrationRepository: yes
- OAuth2AuthorizedClientService: yes
- OAuth2AuthorizedClientRepository: yes
- ReactiveClientRegistrationRepository: no
- ReactiveOAuth2AuthorizedClientService: no
- ServerOAuth2AuthorizedClientRepository: no
- Reactive applications
- ClientRegistrationRepository: no
- OAuth2AuthorizedClientService: no
- OAuth2AuthorizedClientRepository: no
- ReactiveClientRegistrationRepository: yes
- ReactiveOAuth2AuthorizedClientService: yes
- ServerOAuth2AuthorizedClientRepository: yes
- "None" applications without reactor-core
- ClientRegistrationRepository: no
- OAuth2AuthorizedClientService: no
- OAuth2AuthorizedClientRepository: no
- ReactiveClientRegistrationRepository: no
- ReactiveOAuth2AuthorizedClientService: no
- ServerOAuth2AuthorizedClientRepository: no
- "None" applications with reactor-core
- ClientRegistrationRepository: no
- OAuth2AuthorizedClientService: no
- OAuth2AuthorizedClientRepository: no
- ReactiveClientRegistrationRepository: yes
- ReactiveOAuth2AuthorizedClientService: yes
- ServerOAuth2AuthorizedClientRepository: yes (not sure if that makes sense, as it needs
ServerWebExchange, which is on the classpath becausespring-security-oauth2-coredepends onspring-web, but there will never be aServerWebExchangeinstance in a reactive non-web app)
Comment From: mhalbritter
ReactiveClientRegistrationRepository and ReactiveOAuth2AuthorizedClientService and ServerOAuth2AuthorizedClientRepository is supplied on non-servlet applications (meaning reactive and "none"). The auto-configuration for that is in the org.springframework.boot.autoconfigure.security.oauth2.client.reactive package - should we change that? I guess not, as reactive both means "reactive web app" and "reactive programming style".
Comment From: mhalbritter
If we now change the condition for ClientRegistrationRepository and OAuth2AuthorizedClientService and OAuth2AuthorizedClientRepository to work for all non-reactive applications (servlet and "none"), then we end up with this:
- Servlet applications
- Unchanged
- Servlet applications with reactor-core
- Unchanged
- Reactive applications
- Unchanged
- "None" applications without reactor-core
- ClientRegistrationRepository: yes
- OAuth2AuthorizedClientService: yes
- OAuth2AuthorizedClientRepository: yes (but needs
HttpServletRequeston the classpath) - ReactiveClientRegistrationRepository: no
- ReactiveOAuth2AuthorizedClientService: no
- ServerOAuth2AuthorizedClientRepository: no
- "None" applications with reactor-core
- ClientRegistrationRepository: yes
- OAuth2AuthorizedClientService: yes
- OAuth2AuthorizedClientRepository: yes (but needs
HttpServletRequeston the classpath) - ReactiveClientRegistrationRepository: yes
- ReactiveOAuth2AuthorizedClientService: yes
- ServerOAuth2AuthorizedClientRepository: yes
Is that what we want?
Comment From: mhalbritter
I'd do this:
ServerOAuth2AuthorizedClientRepositoryis conditional on reactive web appsOAuth2AuthorizedClientRepositoryis conditional on servlet web apps
That'd give us this:
- Servlet applications
- Unchanged
- Servlet applications with reactor-core
- Unchanged
- Reactive applications
- Unchanged
- "None" applications without reactor-core
- ClientRegistrationRepository: yes
- OAuth2AuthorizedClientService: yes
- OAuth2AuthorizedClientRepository: no
- ReactiveClientRegistrationRepository: no
- ReactiveOAuth2AuthorizedClientService: no
- ServerOAuth2AuthorizedClientRepository: no
- "None" applications with reactor-core
- ClientRegistrationRepository: yes
- OAuth2AuthorizedClientService: yes
- OAuth2AuthorizedClientRepository: no
- ReactiveClientRegistrationRepository: yes
- ReactiveOAuth2AuthorizedClientService: yes
- ServerOAuth2AuthorizedClientRepository: no
And taking https://github.com/spring-projects/spring-boot/issues/40997#issuecomment-2230663273 in account, I'd move org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration to org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration (remove servlet from the package name).