Skip to content
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

OAuth2ClientAutoConfiguration Conditional on Servlet Application #40997

Open
mcordeiro73 opened this issue Jun 5, 2024 · 2 comments
Open

OAuth2ClientAutoConfiguration Conditional on Servlet Application #40997

mcordeiro73 opened this issue Jun 5, 2024 · 2 comments
Labels
status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team status: waiting-for-triage An issue we've not yet triaged

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

No branches or pull requests

3 participants