From 9b89fc2f1fe193157f79f55cbbe4f8f599c7f7f5 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:44:44 -0500 Subject: [PATCH] Add example for setting up client credentials Closes gh-15304 --- .../ROOT/pages/reactive/oauth2/index.adoc | 18 +- .../ROOT/pages/servlet/oauth2/index.adoc | 424 ++++++++++++------ 2 files changed, 294 insertions(+), 148 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc index 0139224ad6..46f6e552ac 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc @@ -69,8 +69,8 @@ See xref:getting-spring-security.adoc[] for additional options when not using Sp Consider the following use cases for OAuth2 Resource Server: -* <> (authorization server provides JWT or opaque access token) -* <> (custom token) +* I want to <> (authorization server provides JWT or opaque access token) +* I want to <> (custom token) [[oauth2-resource-server-access-token]] === Protect Access with an OAuth2 Access Token @@ -393,13 +393,13 @@ See xref:getting-spring-security.adoc[] for additional options when not using Sp Consider the following use cases for OAuth2 Client: -* <> -* <> -* <> (log users in _and_ access a third-party API) -* <> -* <> -* <> -* <> +* I want to <> +* I want to <> in order to access a third-party API +* I want to <> (log users in _and_ access a third-party API) +* I want to <> +* I want to <> +* I want to <> +* I want to <> [[oauth2-client-log-users-in]] === Log Users In with OAuth2 diff --git a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc index 6dcb167e85..5207c9c15f 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc @@ -68,8 +68,8 @@ See xref:getting-spring-security.adoc[] for additional options when not using Sp Consider the following use cases for OAuth2 Resource Server: -* <> (authorization server provides JWT or opaque access token) -* <> (custom token) +* I want to <> (authorization server provides JWT or opaque access token) +* I want to <> (custom token) [[oauth2-resource-server-access-token]] === Protect Access with an OAuth2 Access Token @@ -399,9 +399,10 @@ See xref:getting-spring-security.adoc[] for additional options when not using Sp Consider the following use cases for OAuth2 Client: * I want to <> -* I want to <> +* I want to <> in order to access a third-party API +* I want to <> in order to access a third-party API * I want to <> (log users in _and_ access a third-party API) -* I want to <> +* I want to <> to obtain a single token per application * I want to <> * I want to <> * I want to <> @@ -694,6 +695,229 @@ class MessagesController(private val restClient: RestClient) { ---- ===== +[[oauth2-client-access-protected-resources-webclient]] +=== Access Protected Resources with `WebClient` + +Making requests to a third party API that is protected by OAuth2 is a core use case of OAuth2 Client. +This is accomplished by authorizing a client (represented by the `OAuth2AuthorizedClient` class in Spring Security) and accessing protected resources by placing a `Bearer` token in the `Authorization` header of an outbound request. + +The following example configures the application to act as an OAuth2 Client capable of requesting protected resources from a third party API: + +.Configure OAuth2 Client +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // ... + .oauth2Client(Customizer.withDefaults()); + return http.build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.security.config.annotation.web.invoke + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + // ... + oauth2Client { } + } + + return http.build() + } + +} +---- +===== + +[NOTE] +==== +The above example does not provide a way to log users in. +You can use any other login mechanism (such as `formLogin()`). +See the <> for an example combining `oauth2Client()` with `oauth2Login()`. +==== + +In addition to the above configuration, the application requires at least one `ClientRegistration` to be configured through the use of a `ClientRegistrationRepository` bean. +The following example configures an `InMemoryClientRegistrationRepository` bean using Spring Boot configuration properties: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + my-oauth2-client: + provider: my-auth-server + client-id: my-client-id + client-secret: my-client-secret + authorization-grant-type: authorization_code + scope: message.read,message.write + provider: + my-auth-server: + issuer-uri: https://my-auth-server.com +---- + +In addition to configuring Spring Security to support OAuth2 Client features, you will also need to decide how you will be accessing protected resources and configure your application accordingly. +Spring Security provides implementations of `OAuth2AuthorizedClientManager` for obtaining access tokens that can be used to access protected resources. + +[TIP] +==== +Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist. +==== + +<>, another way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`. +To use `WebClient`, you will need to add the `spring-webflux` dependency along with a reactive client implementation: + +.Add Spring WebFlux Dependency +[tabs] +====== +Gradle:: ++ +[source,gradle,role="primary"] +---- +implementation 'org.springframework:spring-webflux' +implementation 'io.projectreactor.netty:reactor-netty' +---- + +Maven:: ++ +[source,maven,role="secondary"] +---- + + org.springframework + spring-webflux + + + io.projectreactor.netty + reactor-netty + +---- +====== + +The following example uses the default `OAuth2AuthorizedClientManager` to configure a `WebClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request: + +.Configure `WebClient` with `ExchangeFilterFunction` +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction filter = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .apply(filter.oauth2Configuration()) + .build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class WebClientConfig { + + @Bean + fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient { + val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + return WebClient.builder() + .apply(filter.oauth2Configuration()) + .build() + } + +} +---- +===== + +This configured `WebClient` can be used as in the following example: + +.Use `WebClient` to Access Protected Resources +[tabs] +===== +Java:: ++ +[source,java,role="primary"] +---- +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId; + +@RestController +public class MessagesController { + + private final WebClient webClient; + + public MessagesController(WebClient webClient) { + this.webClient = webClient; + } + + @GetMapping("/messages") + public ResponseEntity> messages() { + return this.webClient.get() + .uri("http://localhost:8090/messages") + .attributes(clientRegistrationId("my-oauth2-client")) + .retrieve() + .toEntityList(Message.class) + .block(); + } + + public record Message(String message) { + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId + +@RestController +class MessagesController(private val webClient: WebClient) { + + @GetMapping("/messages") + fun messages(): ResponseEntity> { + return webClient.get() + .uri("http://localhost:8090/messages") + .attributes(clientRegistrationId("my-oauth2-client")) + .retrieve() + .toEntityList() + .block()!! + } + + data class Message(val message: String) + +} +---- +===== + [[oauth2-client-access-protected-resources-current-user]] === Access Protected Resources for the Current User @@ -921,128 +1145,36 @@ Unlike the <> for an example combining `oauth2Client()` with `oauth2Login()`. +This section focuses on additional considerations for the client credentials grant type. +See <> for general setup and usage with all grant types. ==== -In addition to the above configuration, the application requires at least one `ClientRegistration` to be configured through the use of a `ClientRegistrationRepository` bean. -The following example configures an `InMemoryClientRegistrationRepository` bean using Spring Boot configuration properties: +The https://tools.ietf.org/html/rfc6749#section-1.3.4[client credentials grant] allows a client to obtain an `access_token` on behalf of itself. +The client credentials grant is a simple flow that does not involve a resource owner (i.e. a user). -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-oauth2-client: - provider: my-auth-server - client-id: my-client-id - client-secret: my-client-secret - authorization-grant-type: authorization_code - scope: message.read,message.write - provider: - my-auth-server: - issuer-uri: https://my-auth-server.com ----- - -In addition to configuring Spring Security to support OAuth2 Client features, you will also need to decide how you will be accessing protected resources and configure your application accordingly. -Spring Security provides implementations of `OAuth2AuthorizedClientManager` for obtaining access tokens that can be used to access protected resources. - -[TIP] +[WARNING] ==== -Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist. +It is important to note that typical use of the client credentials grant implies that any request (or user) can potentially obtain an access token and make protected resources requests to a resource server. +Exercise caution when designing applications to ensure that users cannot make unauthorized requests since every request will be able to obtain an access token. ==== -Another way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`. -To use `WebClient`, you will need to add the `spring-webflux` dependency along with a reactive client implementation: +When obtaining access tokens within a web application where users can log in, the default behavior of Spring Security is to obtain an access token per user. -.Add Spring WebFlux Dependency -[tabs] -====== -Gradle:: -+ -[source,gradle,role="primary"] ----- -implementation 'org.springframework:spring-webflux' -implementation 'io.projectreactor.netty:reactor-netty' ----- +[NOTE] +==== +By default, access tokens are scoped to the principal name of the current user which means every user will receive a unique access token. +==== -Maven:: -+ -[source,maven,role="secondary"] ----- - - org.springframework - spring-webflux - - - io.projectreactor.netty - reactor-netty - ----- -====== +Clients using the client credentials grant typically require access tokens to be scoped to the application instead of to individual users so there is only one access token per application. +In order to scope access tokens to the application, you will need to set a strategy for resolving a custom principal name. +The following example does this by configuring a `RestClient` with the `RequestAttributePrincipalResolver`: -The following example uses the default `OAuth2AuthorizedClientManager` to configure a `WebClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request: - -.Configure `WebClient` with `ExchangeFilterFunction` +.Configure `RestClient` for `client_credentials` [tabs] ===== Java:: @@ -1050,14 +1182,15 @@ Java:: [source,java,role="primary"] ---- @Configuration -public class WebClientConfig { +public class RestClientConfig { @Bean - public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction filter = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .apply(filter.oauth2Configuration()) + public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) { + OAuth2ClientHttpRequestInterceptor requestInterceptor = + new OAuth2ClientHttpRequestInterceptor(authorizedClientManager); + requestInterceptor.setPrincipalResolver(new RequestAttributePrincipalResolver()); + return RestClient.builder() + .requestInterceptor(requestInterceptor) .build(); } @@ -1069,13 +1202,14 @@ Kotlin:: [source,kotlin,role="secondary"] ---- @Configuration -class WebClientConfig { +class RestClientConfig { @Bean - fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient { - val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .apply(filter.oauth2Configuration()) + fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient { + val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager) + requestInterceptor.setPrincipalResolver(RequestAttributePrincipalResolver()) + return RestClient.builder() + .requestInterceptor(requestInterceptor) .build() } @@ -1083,34 +1217,37 @@ class WebClientConfig { ---- ===== -This configured `WebClient` can be used as in the following example: +With the above configuration in place, a principal name can be specified for each request. +The following example demonstrates how to scope access tokens to the application by specifying a principal name: -.Use `WebClient` to Access Protected Resources +.Scope Access Tokens to the Application [tabs] ===== Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId; +import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId; +import static org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal; @RestController public class MessagesController { - private final WebClient webClient; + private final RestClient restClient; - public MessagesController(WebClient webClient) { - this.webClient = webClient; + public MessagesController(RestClient restClient) { + this.restClient = restClient; } @GetMapping("/messages") public ResponseEntity> messages() { - return this.webClient.get() + Message[] messages = this.restClient.get() .uri("http://localhost:8090/messages") .attributes(clientRegistrationId("my-oauth2-client")) + .attributes(principal("my-application")) .retrieve() - .toEntityList(Message.class) - .block(); + .body(Message[].class); + return ResponseEntity.ok(Arrays.asList(messages)); } public record Message(String message) { @@ -1123,19 +1260,23 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId +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 @RestController -class MessagesController(private val webClient: WebClient) { +class MessagesController(private val restClient: RestClient) { @GetMapping("/messages") fun messages(): ResponseEntity> { - return webClient.get() + val messages = restClient.get() .uri("http://localhost:8090/messages") .attributes(clientRegistrationId("my-oauth2-client")) + .attributes(principal("my-application")) .retrieve() - .toEntityList() - .block()!! + .body>()!! + .toList() + return ResponseEntity.ok(messages) } data class Message(val message: String) @@ -1144,6 +1285,11 @@ class MessagesController(private val webClient: WebClient) { ---- ===== +[NOTE] +==== +When specifying a principal name via attributes as in the above example, there will only be a single access token and it will be used for all requests. +==== + [[oauth2-client-enable-extension-grant-type]] === Enable an Extension Grant Type