From d0fc4fe4dc59f750a3a8965ea46f136c54afd623 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:14:31 -0500 Subject: [PATCH] Document RestClient integration Closes gh-15894 --- .../oauth2/client/authorized-clients.adoc | 444 ++++++++++++++++++ .../pages/servlet/oauth2/client/index.adoc | 1 + 2 files changed, 445 insertions(+) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc index 16ba1f328f..440cedf109 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc @@ -51,6 +51,450 @@ class OAuth2ClientController { The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses an xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[`OAuth2AuthorizedClientManager`] and, therefore, inherits its capabilities. +[[oauth2-client-rest-client]] +== RestClient Integration + +Support for `RestClient` is provided by `OAuth2ClientHttpRequestInterceptor`. +This interceptor provides the ability to make protected resources requests by placing a `Bearer` token in the `Authorization` header of an outbound request. +The interceptor directly uses an `OAuth2AuthorizedClientManager` and therefore inherits the following capabilities: + +* Performs an OAuth 2.0 Access Token request to obtain `OAuth2AccessToken` if the client has not yet been authorized +** `authorization_code`: Triggers the Authorization Request redirect to initiate the flow +** `client_credentials`: The access token is obtained directly from the Token Endpoint +** `password`: The access token is obtained directly from the Token Endpoint +** Additional grant types are supported by xref:servlet/oauth2/index.adoc#oauth2-client-enable-extension-grant-type[enabling extension grant types] +* If an existing `OAuth2AccessToken` is expired, it is refreshed (or renewed) + +The following example uses the default `OAuth2AuthorizedClientManager` to configure a `RestClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request: + +.Configure `RestClient` with `ClientHttpRequestInterceptor` +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) { + OAuth2ClientHttpRequestInterceptor requestInterceptor = + new OAuth2ClientHttpRequestInterceptor(authorizedClientManager); + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class RestClientConfig { + + @Bean + fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient { + val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager) + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build() + } + +} +---- +===== + +[[oauth2-client-rest-client-registration-id]] +=== Providing the `clientRegistrationId` + +`OAuth2ClientHttpRequestInterceptor` uses a `ClientRegistrationIdResolver` to determine which client is used to obtain an access token. +By default, `RequestAttributeClientRegistrationIdResolver` is used to resolve the `clientRegistrationId` from `HttpRequest#attributes()`. + +The following example demonstrates providing a `clientRegistrationId` via attributes: + +.Provide `clientRegistrationId` via attributes +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId; + +@Controller +public class ResourceController { + + private final RestClient restClient; + + public ResourceController(RestClient restClient) { + this.restClient = restClient; + } + + @GetMapping("/") + public String index() { + String resourceUri = "..."; + + String body = this.restClient.get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) // <1> + .retrieve() + .body(String.class); + + // ... + + return "index"; + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId +import org.springframework.web.client.body + +@Controller +class ResourceController(private restClient: RestClient) { + + @GetMapping("/") + fun index(): String { + val resourceUri = "..." + + val body: String = restClient.get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) // <1> + .retrieve() + .body() + + // ... + + return "index" + } + +} +---- +====== +<1> `clientRegistrationId()` is a `static` method in `RequestAttributeClientRegistrationIdResolver`. + +Alternatively, a custom `ClientRegistrationIdResolver` can be provided. +The following example configures a custom implementation that resolves the `clientRegistrationId` from the current user. + +.Configure `ClientHttpRequestInterceptor` with custom `ClientRegistrationIdResolver` +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) { + OAuth2ClientHttpRequestInterceptor requestInterceptor = + new OAuth2ClientHttpRequestInterceptor(authorizedClientManager); + requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver()); + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build(); + } + + private static ClientRegistrationIdResolver clientRegistrationIdResolver() { + return (request) -> { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return (authentication instanceof OAuth2AuthenticationToken principal) + ? principal.getAuthorizedClientRegistrationId() : null; + }; + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class RestClientConfig { + + @Bean + fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient { + val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager) + requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver()) + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build() + } + + fun clientRegistrationIdResolver(): ClientRegistrationIdResolver { + return ClientRegistrationIdResolver { request -> + val authentication = SecurityContextHolder.getContext().getAuthentication() + return if (authentication instanceof OAuth2AuthenticationToken) { + authentication.getAuthorizedClientRegistrationId() + } else { + null + } + } + } + +} +---- +===== + +[[oauth2-client-rest-client-principal]] +=== Providing the `principal` + +`OAuth2ClientHttpRequestInterceptor` uses a `PrincipalResolver` to determine which principal name is associated with the access token, which allows an application to choose how to scope the `OAuth2AuthorizedClient` that is stored. +By default, `SecurityContextHolderPrincipalResolver` is used to resolve the current `principal` from the `SecurityContextHolder`. + +Alternatively, the `principal` can be resolved from `HttpRequest#attributes()` by configuring `RequestAttributePrincipalResolver`, as the following example shows: + +.Configure `ClientHttpRequestInterceptor` with `RequestAttributePrincipalResolver` +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) { + OAuth2ClientHttpRequestInterceptor requestInterceptor = + new OAuth2ClientHttpRequestInterceptor(authorizedClientManager); + requestInterceptor.setPrincipalResolver(new RequestAttributePrincipalResolver()); + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class RestClientConfig { + + @Bean + fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient { + val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager) + requestInterceptor.setPrincipalResolver(RequestAttributePrincipalResolver()) + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build() + } + +} +---- +===== + +The following example demonstrates providing a `principal` name via attributes that scopes the `OAuth2AuthorizedClient` to the application instead of the current user: + +.Provide `principal` name via attributes +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId; +import static org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal; + +@Controller +public class ResourceController { + + private final RestClient restClient; + + public ResourceController(RestClient restClient) { + this.restClient = restClient; + } + + @GetMapping("/") + public String index() { + String resourceUri = "..."; + + String body = this.restClient.get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) + .attributes(principal("my-application")) // <1> + .retrieve() + .body(String.class); + + // ... + + return "index"; + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId +import org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal +import org.springframework.web.client.body + +@Controller +class ResourceController(private restClient: RestClient) { + + @GetMapping("/") + fun index(): String { + val resourceUri = "..." + + val body: String = restClient.get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) + .attributes(principal("my-application")) // <1> + .retrieve() + .body() + + // ... + + return "index" + } + +} +---- +====== +<1> `principal()` is a `static` method in `RequestAttributePrincipalResolver`. + +[[oauth2-client-rest-client-authorization-failure-handler]] +=== Handling Failure + +If an access token is invalid for any reason (e.g. expired token), it can be beneficial to handle the failure by removing the access token so that it cannot be used again. +You can set up the interceptor to do this automatically by providing an `OAuth2AuthorizationFailureHandler` to remove the access token. + +The following example uses an `OAuth2AuthorizedClientRepository` to set up an `OAuth2AuthorizationFailureHandler` that removes an invalid `OAuth2AuthorizedClient` *within* the context of an `HttpServletRequest`: + +.Configure `OAuth2AuthorizationFailureHandler` using `OAuth2AuthorizedClientRepository` +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2ClientHttpRequestInterceptor requestInterceptor = + new OAuth2ClientHttpRequestInterceptor(authorizedClientManager); + + OAuth2AuthorizationFailureHandler authorizationFailureHandler = + OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository); + requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler); + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class RestClientConfig { + + @Bean + fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager, + authorizedClientRepository: OAuth2AuthorizedClientRepository): RestClient { + + val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager) + + val authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor + .authorizationFailureHandler(authorizedClientRepository) + requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler) + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build() + } + +} +---- +===== + +Alternatively, an `OAuth2AuthorizedClientService` can be used to remove an invalid `OAuth2AuthorizedClient` *outside* the context of an `HttpServletRequest`, as the following example shows: + +.Configure `OAuth2AuthorizationFailureHandler` using `OAuth2AuthorizedClientService` +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager, + OAuth2AuthorizedClientService authorizedClientService) { + + OAuth2ClientHttpRequestInterceptor requestInterceptor = + new OAuth2ClientHttpRequestInterceptor(authorizedClientManager); + + OAuth2AuthorizationFailureHandler authorizationFailureHandler = + OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientService); + requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler); + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class RestClientConfig { + + @Bean + fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager, + authorizedClientService: OAuth2AuthorizedClientService): RestClient { + + val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager) + + val authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor + .authorizationFailureHandler(authorizedClientService) + requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler) + + return RestClient.builder() + .requestInterceptor(requestInterceptor) + .build() + } + +} +---- +===== [[oauth2Client-webclient-servlet]] == WebClient Integration for Servlet Environments diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc index 565a719aa8..178c4b78d5 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc @@ -18,6 +18,7 @@ At a high-level, the core features available are: * https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] .HTTP Client support +* xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-rest-client[`RestClient` integration] (for requesting protected resources) * xref:servlet/oauth2/client/authorized-clients.adoc#oauth2Client-webclient-servlet[`WebClient` integration for Servlet Environments] (for requesting protected resources) The `HttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client.