Ver Fonte

Add example for setting up client credentials

Closes gh-15304
Steve Riesenberg há 10 meses atrás
pai
commit
9b89fc2f1f

+ 9 - 9
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:
 
-* <<oauth2-resource-server-access-token,I want to protect access to the API using OAuth2>> (authorization server provides JWT or opaque access token)
-* <<oauth2-resource-server-custom-jwt,I want to protect access to the API using a JWT>> (custom token)
+* I want to <<oauth2-resource-server-access-token,protect access to the API using OAuth2>> (authorization server provides JWT or opaque access token)
+* I want to <<oauth2-resource-server-custom-jwt,protect access to the API using a JWT>> (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:
 
-* <<oauth2-client-log-users-in,I want to log users in using OAuth 2.0 or OpenID Connect 1.0>>
-* <<oauth2-client-access-protected-resources,I want to obtain an access token for users in order to access a third-party API>>
-* <<oauth2-client-access-protected-resources-current-user,I want to do both>> (log users in _and_ access a third-party API)
-* <<oauth2-client-enable-extension-grant-type,I want to enable an extension grant type>>
-* <<oauth2-client-customize-existing-grant-type,I want to customize an existing grant type>>
-* <<oauth2-client-customize-request-parameters,I want to customize token request parameters>>
-* <<oauth2-client-customize-web-client,I want to customize the `WebClient` used by OAuth2 Client components>>
+* I want to <<oauth2-client-log-users-in,log users in using OAuth 2.0 or OpenID Connect 1.0>>
+* I want to <<oauth2-client-access-protected-resources,obtain an access token for users>> in order to access a third-party API
+* I want to <<oauth2-client-access-protected-resources-current-user,do both>> (log users in _and_ access a third-party API)
+* I want to <<oauth2-client-enable-extension-grant-type,enable an extension grant type>>
+* I want to <<oauth2-client-customize-existing-grant-type,customize an existing grant type>>
+* I want to <<oauth2-client-customize-request-parameters,customize token request parameters>>
+* I want to <<oauth2-client-customize-web-client,customize the `WebClient` used by OAuth2 Client components>>
 
 [[oauth2-client-log-users-in]]
 === Log Users In with OAuth2

+ 285 - 139
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:
 
-* <<oauth2-resource-server-access-token,I want to protect access to the API using OAuth2>> (authorization server provides JWT or opaque access token)
-* <<oauth2-resource-server-custom-jwt,I want to protect access to the API using a JWT>> (custom token)
+* I want to <<oauth2-resource-server-access-token,protect access to the API using OAuth2>> (authorization server provides JWT or opaque access token)
+* I want to <<oauth2-resource-server-custom-jwt,protect access to the API using a JWT>> (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 <<oauth2-client-log-users-in,log users in using OAuth 2.0 or OpenID Connect 1.0>>
-* I want to <<oauth2-client-access-protected-resources,use `RestClient` to obtain an access token for users in order to access a third-party API>>
+* I want to <<oauth2-client-access-protected-resources,use `RestClient` to obtain an access token for users>> in order to access a third-party API
+* I want to <<oauth2-client-access-protected-resources-webclient,use `WebClient` to obtain an access token for users>> in order to access a third-party API
 * I want to <<oauth2-client-access-protected-resources-current-user,do both>> (log users in _and_ access a third-party API)
-* I want to <<oauth2-client-access-protected-resources-webclient,use `WebClient` to obtain an access token for users in order to access a third-party API>>
+* I want to <<oauth2-client-client-credentials,use the `client_credentials` grant type>> to obtain a single token per application
 * I want to <<oauth2-client-enable-extension-grant-type,enable an extension grant type>>
 * I want to <<oauth2-client-customize-existing-grant-type,customize an existing grant type>>
 * I want to <<oauth2-client-customize-request-parameters,customize token request parameters>>
@@ -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 <<oauth2-client-access-protected-resources-current-user,previous section>> 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.
+====
+
+<<oauth2-client-access-protected-resources,Instead of configuring a `RestClient`>>, 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"]
+----
+<dependency>
+	<groupId>org.springframework</groupId>
+	<artifactId>spring-webflux</artifactId>
+</dependency>
+<dependency>
+	<groupId>io.projectreactor.netty</groupId>
+	<artifactId>reactor-netty</artifactId>
+</dependency>
+----
+======
+
+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<List<Message>> 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<List<Message>> {
+		return webClient.get()
+			.uri("http://localhost:8090/messages")
+			.attributes(clientRegistrationId("my-oauth2-client"))
+			.retrieve()
+			.toEntityList<Message>()
+			.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 <<oauth2-client-accessing-protected-resources-example,previous exampl
 This is because it can be derived from the currently logged in user.
 ====
 
-[[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()
-	}
-
-}
-----
-=====
+[[oauth2-client-client-credentials]]
+=== Use the Client Credentials Grant
 
 [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 <<oauth2-client-access-protected-resources-current-user,previous section>> for an example combining `oauth2Client()` with `oauth2Login()`.
+This section focuses on additional considerations for the client credentials grant type.
+See <<oauth2-client-access-protected-resources>> 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:
-
-[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.
+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).
 
-[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'
-----
-
-Maven::
-+
-[source,maven,role="secondary"]
-----
-<dependency>
-	<groupId>org.springframework</groupId>
-	<artifactId>spring-webflux</artifactId>
-</dependency>
-<dependency>
-	<groupId>io.projectreactor.netty</groupId>
-	<artifactId>reactor-netty</artifactId>
-</dependency>
-----
-======
+[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.
+====
 
-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:
+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`:
 
-.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<List<Message>> 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<List<Message>> {
-		return webClient.get()
+		val messages = restClient.get()
 			.uri("http://localhost:8090/messages")
 			.attributes(clientRegistrationId("my-oauth2-client"))
+			.attributes(principal("my-application"))
 			.retrieve()
-			.toEntityList<Message>()
-			.block()!!
+			.body<Array<Message>>()!!
+			.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