Skip to content

Commit f3c745c

Browse files
committed
Add reference documentation for Token Exchange
Closes gh-14698
1 parent be340a0 commit f3c745c

File tree

4 files changed

+465
-0
lines changed

4 files changed

+465
-0
lines changed

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

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,3 +1156,215 @@ class OAuth2ResourceServerController {
11561156

11571157
[TIP]
11581158
If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerReactiveOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function<OAuth2AuthorizationContext, Mono<Jwt>>`.
1159+
1160+
[[oauth2Client-token-exchange-grant]]
1161+
== Token Exchange
1162+
1163+
[NOTE]
1164+
Please refer to OAuth 2.0 Token Exchange for further details on the https://datatracker.ietf.org/doc/html/rfc8693[Token Exchange] grant.
1165+
1166+
1167+
=== Requesting an Access Token
1168+
1169+
[NOTE]
1170+
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.
1171+
1172+
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.
1173+
1174+
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.
1175+
1176+
1177+
=== Customizing the Access Token Request
1178+
1179+
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>>`.
1180+
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.
1181+
Other parameters required by the Token Exchange grant are added directly to the body of the request by the `WebClientReactiveTokenExchangeTokenResponseClient`.
1182+
However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s).
1183+
1184+
[TIP]
1185+
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`.
1186+
1187+
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.
1188+
1189+
=== Customizing the Access Token Response
1190+
1191+
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`.
1192+
The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly.
1193+
1194+
=== Customizing the `WebClient`
1195+
1196+
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`.
1197+
1198+
Whether you customize `WebClientReactiveTokenExchangeTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example:
1199+
1200+
[tabs]
1201+
======
1202+
Java::
1203+
+
1204+
[source,java,role="primary"]
1205+
----
1206+
// Customize
1207+
ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeTokenResponseClient = ...
1208+
1209+
TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
1210+
tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeTokenResponseClient);
1211+
1212+
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
1213+
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
1214+
.provider(tokenExchangeAuthorizedClientProvider)
1215+
.build();
1216+
1217+
...
1218+
1219+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
1220+
----
1221+
1222+
Kotlin::
1223+
+
1224+
[source,kotlin,role="secondary"]
1225+
----
1226+
// Customize
1227+
val tokenExchangeTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> = ...
1228+
1229+
val tokenExchangeAuthorizedClientProvider = TokenExchangeReactiveOAuth2AuthorizedClientProvider()
1230+
tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeTokenResponseClient)
1231+
1232+
val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
1233+
.provider(tokenExchangeAuthorizedClientProvider)
1234+
.build()
1235+
1236+
...
1237+
1238+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
1239+
----
1240+
======
1241+
1242+
=== Using the Access Token
1243+
1244+
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
1245+
1246+
[source,yaml]
1247+
----
1248+
spring:
1249+
security:
1250+
oauth2:
1251+
client:
1252+
registration:
1253+
okta:
1254+
client-id: okta-client-id
1255+
client-secret: okta-client-secret
1256+
authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
1257+
scope: read
1258+
provider:
1259+
okta:
1260+
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
1261+
----
1262+
1263+
...and the `OAuth2AuthorizedClientManager` `@Bean`:
1264+
1265+
[tabs]
1266+
======
1267+
Java::
1268+
+
1269+
[source,java,role="primary"]
1270+
----
1271+
@Bean
1272+
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
1273+
ReactiveClientRegistrationRepository clientRegistrationRepository,
1274+
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
1275+
1276+
TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
1277+
new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
1278+
1279+
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
1280+
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
1281+
.provider(tokenExchangeAuthorizedClientProvider)
1282+
.build();
1283+
1284+
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
1285+
new DefaultReactiveOAuth2AuthorizedClientManager(
1286+
clientRegistrationRepository, authorizedClientRepository);
1287+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
1288+
1289+
return authorizedClientManager;
1290+
}
1291+
----
1292+
1293+
Kotlin::
1294+
+
1295+
[source,kotlin,role="secondary"]
1296+
----
1297+
@Bean
1298+
fun authorizedClientManager(
1299+
clientRegistrationRepository: ReactiveClientRegistrationRepository,
1300+
authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager {
1301+
val tokenExchangeAuthorizedClientProvider = TokenExchangeReactiveOAuth2AuthorizedClientProvider()
1302+
val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
1303+
.provider(tokenExchangeAuthorizedClientProvider)
1304+
.build()
1305+
val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager(
1306+
clientRegistrationRepository, authorizedClientRepository)
1307+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
1308+
return authorizedClientManager
1309+
}
1310+
----
1311+
======
1312+
1313+
You may obtain the `OAuth2AccessToken` as follows:
1314+
1315+
[tabs]
1316+
======
1317+
Java::
1318+
+
1319+
[source,java,role="primary"]
1320+
----
1321+
@RestController
1322+
public class OAuth2ResourceServerController {
1323+
1324+
@Autowired
1325+
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
1326+
1327+
@GetMapping("/resource")
1328+
public Mono<String> resource(JwtAuthenticationToken jwtAuthentication) {
1329+
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
1330+
.principal(jwtAuthentication)
1331+
.build();
1332+
1333+
return this.authorizedClientManager.authorize(authorizeRequest)
1334+
.map(OAuth2AuthorizedClient::getAccessToken)
1335+
...
1336+
}
1337+
}
1338+
----
1339+
1340+
Kotlin::
1341+
+
1342+
[source,kotlin,role="secondary"]
1343+
----
1344+
class OAuth2ResourceServerController {
1345+
1346+
@Autowired
1347+
private lateinit var authorizedClientManager: ReactiveOAuth2AuthorizedClientManager
1348+
1349+
@GetMapping("/resource")
1350+
fun resource(jwtAuthentication: JwtAuthenticationToken): Mono<String> {
1351+
val authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
1352+
.principal(jwtAuthentication)
1353+
.build()
1354+
return authorizedClientManager.authorize(authorizeRequest)
1355+
.map { it.accessToken }
1356+
...
1357+
}
1358+
}
1359+
----
1360+
======
1361+
1362+
[NOTE]
1363+
`TokenExchangeReactiveOAuth2AuthorizedClientProvider` resolves the subject token (as an `OAuth2Token`) via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example.
1364+
An actor token is not resolved by default.
1365+
1366+
[TIP]
1367+
If you need to resolve the subject token from a different source, you can provide `TokenExchangeReactiveOAuth2AuthorizedClientProvider.setSubjectTokenResolver()` with a custom `Function<OAuth2AuthorizationContext, Mono<OAuth2Token>>`.
1368+
1369+
[TIP]
1370+
If you need to resolve an actor token, you can provide `TokenExchangeReactiveOAuth2AuthorizedClientProvider.setActorTokenResolver()` with a custom `Function<OAuth2AuthorizationContext, Mono<OAuth2Token>>`.

docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ At a high-level, the core features available are:
1212
* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials]
1313
* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials]
1414
* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer]
15+
* https://datatracker.ietf.org/doc/html/rfc8693#section-2.1[Token Exchange]
1516

1617
.Client Authentication support
1718
* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer]

0 commit comments

Comments
 (0)