-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
Add support for requesting protected resources with RestClient #13588
Comments
@mjeffrey thanks for starting the conversation on this! To clarify, are you specifically directing this issue toward providing support for |
@sjohnr What I'd like to achieve is to be able to use the new RestClient with the Oauth2 client and to not need the reactive libraries. In my company we are in the process of migrating a number of Spring Boot 2 (keycloak Oauth2 client) to Spring Boot 3 projects. |
Ok, I've changed the title of the issue to what I think aligns with the request as a starting point, and we can go from there. Since the only part of OAuth2 Client for a Servlet environment that directly uses reactive libraries is |
I was about to create a similar issue, glad someone already did. Many thanks @mjeffrey. Since support for RestTemplate was dropped because it is now in maintenance mode, there should now be support for the new RestClient for Servlet applications without using WebClient in blocking mode. |
I am maintaining a library, which provides a blocking WebClient with OAuth2 client credentials support and also want to migrate to the new RestClient. At the moment the OAuth2 support in this library is built with Will there be also non-reactive equivalents for ServerOAuth2AuthorizedClientExchangeFilterFunction? |
@tudburley we already have ServletOAuth2AuthorizedClientExchangeFilterFunction, is that what you're asking about? Otherwise, I'm not sure I understand the question. If you have a different enhancement suggestion than what is being discussed here, feel free to open a new issue with the details. |
Its hard to understand what is the difference of ServletOAuth2AuthorizedClientExchangeFilterFunction and ServerOAuth2AuthorizedClientExchangeFilterFunction. My guess is, that the servlet variant stores authorized clients in the servlet context or to the current servlet request attributes. But I am maintaining a web client library, and I am confused why I should use a ServletOAuth2AuthorizedClientExchangeFilterFunction and coupling it to servlets. This client library should by used by any Spring Boot application, even non-servlet applications. And even if I would switch to ServletOAuth2AuthorizedClientExchangeFilterFunction: This class also depends on reactive classes. So I wouldn't get rid off the reactive stuff. |
Ah, thanks for clarifying @tudburley!
Without some extra context, it is indeed confusing. The difference is in which components are utilized internally to process the request. Spring Security (and Spring more widely) provides support for both reactive and non-reactive stacks and they have completely separate interfaces/implementations/etc. There's probably a better guide on the history and naming conventions, but typically "servlet" refers to the imperative stack and "reactive" obviously refers to the reactive stack. However, some components use the name "server" to also refer to the reactive stack. It just so happens that both implementations can be plugged into a
I now understand your question. Such support would be possible only with this issue, through the use of a There is no planned support for using these underlying components with a |
Hi @sjohnr for the moment i'm doing it in this way : @Bean("client-credentials")
RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager,
ClientRegistrationRepository clientRegistrationRepository) {
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("registration-id");
ClientHttpRequestInterceptor oauthInterceptor = new OAuthClientCredentialsRestTemplateInterceptor(authorizedClientManager, clientRegistration);
return RestClient.builder()
.requestInterceptor(oauthInterceptor)
.build();
} and in my inteceptor i call the authorization method to fetch a token and add it as a header @Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(principal)
.build();
OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
if (isNull(client)) {
throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
}
System.out.println("Bearer " + client.getAccessToken().getTokenValue() +
" - issued at : " + client.getAccessToken().getIssuedAt() +
" - expired at : " + client.getAccessToken().getExpiresAt()
);
request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + client.getAccessToken().getTokenValue());
return execution.execute(request, body);
} The question is why we need to do it manually ? it could be better if it is handled by spring? Regards |
I'm also very much waiting when the new |
May I please have an update on this matter? |
Making a custom interceptor is simple enough to do (see for example rawaldop's comment) but since Spring Security is a framework its components have to be able to function in a variety of different setups. For example, the reactive webClient
.get()
.uri(uri)
.attributes(clientRegistrationId("my-special-client-registration"))
// ...
.retrieve()
.bodyToMono(String.class); Neither |
Could you provide the full implementation for this please? |
@mohmf I do believe that is the full implementation, except they've left out the interceptor's constructor and the definition of the public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final ClientRegistration clientRegistration;
public OAuthClientCredentialsRestTemplateInterceptor(
OAuth2AuthorizedClientManager authorizedClientManager, ClientRegistration clientRegistration) {
this.authorizedClientManager = authorizedClientManager;
this.clientRegistration = clientRegistration;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
var clientRegistrationId = this.clientRegistration.getRegistrationId();
var principal = this.clientRegistration.getClientId();
var authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistrationId)
.principal(principal)
.build();
var authorizedClient = manager.authorize(authorizeRequest);
if (authorizedClient == null) {
throw new IllegalStateException("client credentials flow on " + clientRegistrationId + " failed, client is null");
}
request.setBearerAuth(client.getAccessToken().getTokenValue());
return execution.execute(request, body);
}
} |
@sjohnr I really like the design of this final proposal, especially the fact that it requires one single interface and that it re-uses the familiar concept of Interceptor (and consistent with |
@sjohnr Your Interceptor is almost identical to what I did. One other thing we created was an ErrorHandler that will remove the authorized client when an authorization error occurs. We interact with some 3rd party services that will revoke a token before it expires, so we have a need to ensure when that occurs we remove the authorize client so the next attempt will result in a new token being requested. Hers is what we did for an ErrorHandler: 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;
}
} With my RestClient config then looking like: @Bean
protected RestClient restClient(RestClient.Builder builder, ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService oauth2AuthorizedClientService) {
OAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oauth2AuthorizedClientService);
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(CLIENT_REGISTRATION_ID);
return builder.requestInterceptor(new Oauth2ClientHttpRequestInterceptor(authorizedClientManager, clientRegistration))
.defaultStatusHandler(RemoveAuthorizedClientOAuth2ResponseErrorHandler.unauthorizedStatusPredicate(), RemoveAuthorizedClientOAuth2ResponseErrorHandler.createErrorHandler(authorizedClientService, clientRegistration))
.build();
} This is by no means a perfect solution as it essentially requires the use of the Removing the authorized client is one thing that used to be automatic with Resttemplate that was lost when moving to WebClient (though there were ways to ensure it was in place) and then to RestClient. Another thing lost was the automatic retry of a request with a new token after an authorization failure (via OAuth2RestTemplate). My hope is that we can bring both these features back to the core Spring libraries as they were very useful in certain situations. |
This comment was marked as off-topic.
This comment was marked as off-topic.
Thanks for the feedback @mcordeiro73! The failure handling is what I wanted to work on next, which I have added in this version. I have further simplified things by dropping support for a separate The per-application configuration looks like this (with only a new setter added):
and a per-request arrangement might look like this:
In the per-request case, we are taking a generic (globally configured) This is necessary since we don't have the ability to intercept the request so we have to specify an error handler separately. The error handler is optional if removal of the authorized client isn't required. |
This comment was marked as off-topic.
This comment was marked as off-topic.
I like this proposed oauth interceptor. Will it be possible to have the interceptor only work for specific endpoints? What is the eta for this issue? |
@barbetb thanks for your interest in this issue!
Yes. See the per-request arrangement example in my above comment.
I've added this issue to the |
@sjohnr - Thanks for the work on this one. I think this solves one of the big use cases for OAuth and RestClient however I think it misses the other one. In situations where our service is called with a token and we want to pass that token on to subsequent calls we have historically built out a WebClient bean like this
and taken advantage of the ServletBearerExchangeFilterFunction to automatically get the user's token and pass that on for subsequent calls. Given the title of this issue (after the rename), I would have expected it to also solve this use case (unless it has and I'm missing something). |
@azizabah thanks for mentioning this. This issue is specifically for the OAuth2 Client use case, though I understand why you feel the title is generic enough it ought to apply to OAuth2 Resource Server. Would you please consider opening a new issue for your use case? I would be happy to take a look at what it would take. |
Expected Behavior
Allow the use RestClient (to be introduced in Spring 6.1) for blocking calls in a non reactive application In Oauth2 Client.
See https://spring.io/blog/2023/07/13/new-in-spring-6-1-restclient.
Current Behavior
Only WebClient is supported which means a lot of reactive dependencies are pulled in when using Oauth2 Client even in a blocking application.
Context
To avoid the dependency issue we are using RestTemplate (with interceptors) but would prefer to see a supported solution.
The text was updated successfully, but these errors were encountered: