Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mcordeiro73 opened this issue Jun 5, 2024 · 15 comments
Assignees
Labels
type: enhancement A general enhancement
Milestone

Comments

@mcordeiro73
Copy link

mcordeiro73 commented Jun 5, 2024

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 5, 2024
@wilkinsona
Copy link
Member

wilkinsona commented Jun 10, 2024

@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?

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Jun 10, 2024
@mcordeiro73
Copy link
Author

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;
	}

}

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 13, 2024
@wilkinsona wilkinsona added status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team and removed status: feedback-provided Feedback has been provided labels Jun 13, 2024
@rwinch
Copy link
Member

rwinch commented Jul 15, 2024

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

@wilkinsona wilkinsona changed the title OAuth2ClientAutoConfiguration Conditional on Servlet Application Broaden OAuth2 client auto-configuration to include non servlet web applications Jul 16, 2024
@wilkinsona wilkinsona removed status: waiting-for-triage An issue we've not yet triaged status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team labels Jul 16, 2024
@wilkinsona wilkinsona added this to the 3.x milestone Jul 16, 2024
@wilkinsona wilkinsona added the type: enhancement A general enhancement label Jul 16, 2024
@wilkinsona
Copy link
Member

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.

@swargolet
Copy link

+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.

@wilkinsona
Copy link
Member

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.

@neoludo
Copy link

neoludo commented Oct 10, 2024

So what is the best way to use OIDC client in a batch ?

@wilkinsona
Copy link
Member

@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.

@neoludo
Copy link

neoludo commented Oct 10, 2024 via email

@wilkinsona
Copy link
Member

wilkinsona commented Oct 10, 2024

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.

@neoludo
Copy link

neoludo commented Oct 11, 2024

Glad to hear that. I just had done a bad interpretation of your last comment last month. Sorry for that.

@mhalbritter mhalbritter self-assigned this Nov 11, 2024
@mhalbritter
Copy link
Contributor

mhalbritter commented Nov 11, 2024

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 because spring-security-oauth2-core depends on spring-web, but there will never be a ServerWebExchange instance in a reactive non-web app)

@mhalbritter
Copy link
Contributor

mhalbritter commented Nov 11, 2024

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".

@mhalbritter
Copy link
Contributor

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 HttpServletRequest on the classpath)
    • ReactiveClientRegistrationRepository: no
    • ReactiveOAuth2AuthorizedClientService: no
    • ServerOAuth2AuthorizedClientRepository: no
  • "None" applications with reactor-core
    • ClientRegistrationRepository: yes
    • OAuth2AuthorizedClientService: yes
    • OAuth2AuthorizedClientRepository: yes (but needs HttpServletRequest on the classpath)
    • ReactiveClientRegistrationRepository: yes
    • ReactiveOAuth2AuthorizedClientService: yes
    • ServerOAuth2AuthorizedClientRepository: yes

Is that what we want?

@mhalbritter
Copy link
Contributor

mhalbritter commented Nov 11, 2024

I'd do this:

  • ServerOAuth2AuthorizedClientRepository is conditional on reactive web apps
  • OAuth2AuthorizedClientRepository is 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 #40997 (comment) 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).

@wilkinsona wilkinsona modified the milestones: 3.x, 3.5.x, 3.5.0-RC1 Apr 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

7 participants