Skip to content

Commit

Permalink
Add reference documentation for Token Exchange
Browse files Browse the repository at this point in the history
Closes gh-14698
  • Loading branch information
sjohnr committed Mar 26, 2024
1 parent be340a0 commit f3c745c
Show file tree
Hide file tree
Showing 4 changed files with 465 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading

0 comments on commit f3c745c

Please sign in to comment.