浏览代码

Add reference documentation for Token Exchange

Closes gh-14698
Steve Riesenberg 1 年之前
父节点
当前提交
f3c745c65b

+ 212 - 0
docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc

@@ -1156,3 +1156,215 @@ class OAuth2ResourceServerController {
 
 [TIP]
 If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerReactiveOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function<OAuth2AuthorizationContext, Mono<Jwt>>`.
+
+[[oauth2Client-token-exchange-grant]]
+== Token Exchange
+
+[NOTE]
+Please refer to OAuth 2.0 Token Exchange for further details on the https://datatracker.ietf.org/doc/html/rfc8693[Token Exchange] grant.
+
+
+=== Requesting an Access Token
+
+[NOTE]
+Please refer to the https://datatracker.ietf.org/doc/html/rfc8693#section-2[Token Exchange Request and Response] protocol flow for the Token Exchange grant.
+
+The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the Token Exchange grant is `WebClientReactiveTokenExchangeTokenResponseClient`, which uses a `WebClient` when requesting an access token at the Authorization Server’s Token Endpoint.
+
+The `WebClientReactiveTokenExchangeTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
+
+
+=== Customizing the Access Token Request
+
+If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveTokenExchangeTokenResponseClient.setParametersConverter()` with a custom `Converter<TokenExchangeGrantRequest, MultiValueMap<String, String>>`.
+The default implementation builds a `MultiValueMap<String, String>` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request.
+Other parameters required by the Token Exchange grant are added directly to the body of the request by the `WebClientReactiveTokenExchangeTokenResponseClient`.
+However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s).
+
+[TIP]
+If you prefer to only add additional parameters, you can instead provide `WebClientReactiveTokenExchangeTokenResponseClient.addParametersConverter()` with a custom `Converter<TokenExchangeGrantRequest, MultiValueMap<String, String>>` which constructs an aggregate `Converter`.
+
+IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider.
+
+=== Customizing the Access Token Response
+
+On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveTokenExchangeTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor<Mono<OAuth2AccessTokenResponse>, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`.
+The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly.
+
+=== Customizing the `WebClient`
+
+Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveTokenExchangeTokenResponseClient.setWebClient()` with a custom configured `WebClient`.
+
+Whether you customize `WebClientReactiveTokenExchangeTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+// Customize
+ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeTokenResponseClient = ...
+
+TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
+tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeTokenResponseClient);
+
+ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
+		ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
+				.provider(tokenExchangeAuthorizedClientProvider)
+				.build();
+
+...
+
+authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+// Customize
+val tokenExchangeTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> = ...
+
+val tokenExchangeAuthorizedClientProvider = TokenExchangeReactiveOAuth2AuthorizedClientProvider()
+tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeTokenResponseClient)
+
+val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
+        .provider(tokenExchangeAuthorizedClientProvider)
+        .build()
+
+...
+
+authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
+----
+======
+
+=== Using the Access Token
+
+Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
+
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      client:
+        registration:
+          okta:
+            client-id: okta-client-id
+            client-secret: okta-client-secret
+            authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
+            scope: read
+        provider:
+          okta:
+            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
+----
+
+...and the `OAuth2AuthorizedClientManager` `@Bean`:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
+		ReactiveClientRegistrationRepository clientRegistrationRepository,
+		ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
+
+	TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
+			new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
+
+	ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
+			ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
+					.provider(tokenExchangeAuthorizedClientProvider)
+					.build();
+
+	DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
+			new DefaultReactiveOAuth2AuthorizedClientManager(
+					clientRegistrationRepository, authorizedClientRepository);
+	authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+
+	return authorizedClientManager;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun authorizedClientManager(
+        clientRegistrationRepository: ReactiveClientRegistrationRepository,
+        authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager {
+    val tokenExchangeAuthorizedClientProvider = TokenExchangeReactiveOAuth2AuthorizedClientProvider()
+    val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
+            .provider(tokenExchangeAuthorizedClientProvider)
+            .build()
+    val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager(
+            clientRegistrationRepository, authorizedClientRepository)
+    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
+    return authorizedClientManager
+}
+----
+======
+
+You may obtain the `OAuth2AccessToken` as follows:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@RestController
+public class OAuth2ResourceServerController {
+
+	@Autowired
+	private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
+
+	@GetMapping("/resource")
+	public Mono<String> resource(JwtAuthenticationToken jwtAuthentication) {
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
+				.principal(jwtAuthentication)
+				.build();
+
+		return this.authorizedClientManager.authorize(authorizeRequest)
+				.map(OAuth2AuthorizedClient::getAccessToken)
+				...
+	}
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+class OAuth2ResourceServerController {
+
+    @Autowired
+    private lateinit var authorizedClientManager: ReactiveOAuth2AuthorizedClientManager
+
+    @GetMapping("/resource")
+    fun resource(jwtAuthentication: JwtAuthenticationToken): Mono<String> {
+        val authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
+                .principal(jwtAuthentication)
+                .build()
+        return authorizedClientManager.authorize(authorizeRequest)
+                .map { it.accessToken }
+                ...
+    }
+}
+----
+======
+
+[NOTE]
+`TokenExchangeReactiveOAuth2AuthorizedClientProvider` resolves the subject token (as an `OAuth2Token`) via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example.
+An actor token is not resolved by default.
+
+[TIP]
+If you need to resolve the subject token from a different source, you can provide `TokenExchangeReactiveOAuth2AuthorizedClientProvider.setSubjectTokenResolver()` with a custom `Function<OAuth2AuthorizationContext, Mono<OAuth2Token>>`.
+
+[TIP]
+If you need to resolve an actor token, you can provide `TokenExchangeReactiveOAuth2AuthorizedClientProvider.setActorTokenResolver()` with a custom `Function<OAuth2AuthorizationContext, Mono<OAuth2Token>>`.

+ 1 - 0
docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc

@@ -12,6 +12,7 @@ At a high-level, the core features available are:
 * https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials]
 * https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials]
 * https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer]
+* https://datatracker.ietf.org/doc/html/rfc8693#section-2.1[Token Exchange]
 
 .Client Authentication support
 * https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer]

+ 251 - 0
docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc

@@ -1435,3 +1435,254 @@ class OAuth2ResourceServerController {
 
 [TIP]
 If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function<OAuth2AuthorizationContext, Jwt>`.
+
+[[oauth2Client-token-exchange-grant]]
+== Token Exchange
+
+[NOTE]
+====
+Please refer to OAuth 2.0 Token Exchange for further details on the https://datatracker.ietf.org/doc/html/rfc8693[Token Exchange] grant.
+====
+
+
+=== Requesting an Access Token
+
+[NOTE]
+====
+Please refer to the https://datatracker.ietf.org/doc/html/rfc8693#section-2[Token Exchange Request and Response] protocol flow for the Token Exchange grant.
+====
+
+The default implementation of `OAuth2AccessTokenResponseClient` for the Token Exchange grant is `DefaultTokenExchangeTokenResponseClient`, which uses a `RestOperations` when requesting an access token at the Authorization Server’s Token Endpoint.
+
+The `DefaultTokenExchangeTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
+
+
+=== Customizing the Access Token Request
+
+If you need to customize the pre-processing of the Token Request, you can provide `DefaultTokenExchangeTokenResponseClient.setRequestEntityConverter()` with a custom `Converter<TokenExchangeGrantRequest, RequestEntity<?>>`.
+The default implementation `TokenExchangeGrantRequestEntityConverter` builds a `RequestEntity` representation of a https://datatracker.ietf.org/doc/html/rfc8693#section-2.1[OAuth 2.0 Access Token Request].
+However, providing a custom `Converter`, would allow you to extend the Token Request and add custom parameter(s).
+
+To customize only the parameters of the request, you can provide `TokenExchangeGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter<TokenExchangeGrantRequest, MultiValueMap<String, String>>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly.
+
+[TIP]
+If you prefer to only add additional parameters, you can provide `TokenExchangeGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter<TokenExchangeGrantRequest, MultiValueMap<String, String>>` which constructs an aggregate `Converter`.
+
+
+=== Customizing the Access Token Response
+
+On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultTokenExchangeTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`.
+The default `RestOperations` is configured as follows:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+RestTemplate restTemplate = new RestTemplate(Arrays.asList(
+		new FormHttpMessageConverter(),
+		new OAuth2AccessTokenResponseHttpMessageConverter()));
+
+restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+val restTemplate = RestTemplate(listOf(
+        FormHttpMessageConverter(),
+        OAuth2AccessTokenResponseHttpMessageConverter()))
+
+restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler()
+----
+======
+
+[TIP]
+====
+Spring MVC `FormHttpMessageConverter` is required as it's used when sending the OAuth 2.0 Access Token Request.
+====
+
+`OAuth2AccessTokenResponseHttpMessageConverter` is a `HttpMessageConverter` for an OAuth 2.0 Access Token Response.
+You can provide `OAuth2AccessTokenResponseHttpMessageConverter.setAccessTokenResponseConverter()` with a custom `Converter<Map<String, Object>, OAuth2AccessTokenResponse>` that is used for converting the OAuth 2.0 Access Token Response parameters to an `OAuth2AccessTokenResponse`.
+
+`OAuth2ErrorResponseErrorHandler` is a `ResponseErrorHandler` that can handle an OAuth 2.0 Error, eg. 400 Bad Request.
+It uses an `OAuth2ErrorHttpMessageConverter` for converting the OAuth 2.0 Error parameters to an `OAuth2Error`.
+
+Whether you customize `DefaultTokenExchangeTokenResponseClient` or provide your own implementation of `OAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+// Customize
+OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeTokenResponseClient = ...
+
+TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
+tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeTokenResponseClient);
+
+OAuth2AuthorizedClientProvider authorizedClientProvider =
+		OAuth2AuthorizedClientProviderBuilder.builder()
+				.provider(tokenExchangeAuthorizedClientProvider)
+				.build();
+
+...
+
+authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+// Customize
+val tokenExchangeTokenResponseClient: OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> = ...
+
+val tokenExchangeAuthorizedClientProvider = TokenExchangeOAuth2AuthorizedClientProvider()
+tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeTokenResponseClient)
+
+val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
+        .provider(tokenExchangeAuthorizedClientProvider)
+        .build()
+
+...
+
+authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
+----
+======
+
+=== Using the Access Token
+
+Given the following Spring Boot properties for an OAuth 2.0 Client registration:
+
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      client:
+        registration:
+          okta:
+            client-id: okta-client-id
+            client-secret: okta-client-secret
+            authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
+            scope: read
+        provider:
+          okta:
+            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
+----
+
+...and the `OAuth2AuthorizedClientManager` `@Bean`:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+public OAuth2AuthorizedClientManager authorizedClientManager(
+		ClientRegistrationRepository clientRegistrationRepository,
+		OAuth2AuthorizedClientRepository authorizedClientRepository) {
+
+	TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
+			new TokenExchangeOAuth2AuthorizedClientProvider();
+
+	OAuth2AuthorizedClientProvider authorizedClientProvider =
+			OAuth2AuthorizedClientProviderBuilder.builder()
+					.provider(tokenExchangeAuthorizedClientProvider)
+					.build();
+
+	DefaultOAuth2AuthorizedClientManager authorizedClientManager =
+			new DefaultOAuth2AuthorizedClientManager(
+					clientRegistrationRepository, authorizedClientRepository);
+	authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+
+	return authorizedClientManager;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun authorizedClientManager(
+        clientRegistrationRepository: ClientRegistrationRepository,
+        authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
+    val tokenExchangeAuthorizedClientProvider = TokenExchangeOAuth2AuthorizedClientProvider()
+    val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
+            .provider(tokenExchangeAuthorizedClientProvider)
+            .build()
+    val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
+            clientRegistrationRepository, authorizedClientRepository)
+    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
+    return authorizedClientManager
+}
+----
+======
+
+You may obtain the `OAuth2AccessToken` as follows:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@RestController
+public class OAuth2ResourceServerController {
+
+	@Autowired
+	private OAuth2AuthorizedClientManager authorizedClientManager;
+
+	@GetMapping("/resource")
+	public String resource(JwtAuthenticationToken jwtAuthentication) {
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
+				.principal(jwtAuthentication)
+				.build();
+		OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
+
+		...
+
+	}
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+class OAuth2ResourceServerController {
+
+    @Autowired
+    private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager
+
+    @GetMapping("/resource")
+    fun resource(jwtAuthentication: JwtAuthenticationToken?): String {
+        val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
+                .principal(jwtAuthentication)
+                .build()
+        val authorizedClient = authorizedClientManager.authorize(authorizeRequest)
+        val accessToken: OAuth2AccessToken = authorizedClient.accessToken
+
+        ...
+
+    }
+}
+----
+======
+
+[NOTE]
+`TokenExchangeOAuth2AuthorizedClientProvider` resolves the subject token (as an `OAuth2Token`) via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example.
+An actor token is not resolved by default.
+
+[TIP]
+If you need to resolve the subject token from a different source, you can provide `TokenExchangeOAuth2AuthorizedClientProvider.setSubjectTokenResolver()` with a custom `Function<OAuth2AuthorizationContext, OAuth2Token>`.
+
+[TIP]
+If you need to resolve an actor token, you can provide `TokenExchangeOAuth2AuthorizedClientProvider.setActorTokenResolver()` with a custom `Function<OAuth2AuthorizationContext, OAuth2Token>`.

+ 1 - 0
docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc

@@ -12,6 +12,7 @@ At a high-level, the core features available are:
 * https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials]
 * https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials]
 * https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer]
+* https://datatracker.ietf.org/doc/html/rfc8693#section-2.1[Token Exchange]
 
 .Client Authentication support
 * https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer]