From f3c745c65b9890e1e8e1013ceeded44bd8e2229b Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:01:09 -0500 Subject: [PATCH] Add reference documentation for Token Exchange Closes gh-14698 --- .../oauth2/client/authorization-grants.adoc | 212 +++++++++++++++ .../pages/reactive/oauth2/client/index.adoc | 1 + .../oauth2/client/authorization-grants.adoc | 251 ++++++++++++++++++ .../pages/servlet/oauth2/client/index.adoc | 1 + 4 files changed, 465 insertions(+) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc index 450485a6466..eb3191d16da 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc +++ b/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>`. + +[[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>`. +The default implementation builds a `MultiValueMap` 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>` 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, 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 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 = ... + +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 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 { + 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>`. + +[TIP] +If you need to resolve an actor token, you can provide `TokenExchangeReactiveOAuth2AuthorizedClientProvider.setActorTokenResolver()` with a custom `Function>`. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc index a0c2d5bc9c1..044d4da4a8a 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc +++ b/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] diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc index 25dd1e8f394..1a080abf83f 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc +++ b/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`. + +[[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>`. +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>` 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>` 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, 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 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 = ... + +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`. + +[TIP] +If you need to resolve an actor token, you can provide `TokenExchangeOAuth2AuthorizedClientProvider.setActorTokenResolver()` with a custom `Function`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc index 3adac445889..565a719aa8c 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc +++ b/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]