From 0681e722e94f4a9fa834aa14a059a006d09b5043 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:03:35 -0600 Subject: [PATCH] Add support for OAuth 2.0 Token Exchange Grant Issue gh-5199 --- ...xchangeOAuth2AuthorizedClientProvider.java | 182 ++++++++++++++++++ ...faultTokenExchangeTokenResponseClient.java | 126 ++++++++++++ .../endpoint/TokenExchangeGrantRequest.java | 76 ++++++++ ...enExchangeGrantRequestEntityConverter.java | 82 ++++++++ .../oauth2/core/AuthorizationGrantType.java | 6 + .../core/endpoint/OAuth2ParameterNames.java | 36 ++++ 6 files changed, 508 insertions(+) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultTokenExchangeTokenResponseClient.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequest.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequestEntityConverter.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java new file mode 100644 index 00000000000..725d83b4f79 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.function.Function; + +import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.client.endpoint.DefaultTokenExchangeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.util.Assert; + +/** + * An implementation of an {@link OAuth2AuthorizedClientProvider} for the + * {@link AuthorizationGrantType#TOKEN_EXCHANGE token-exchange} grant. + * + * @author Steve Riesenberg + * @since 6.3 + * @see OAuth2AuthorizedClientProvider + * @see DefaultTokenExchangeTokenResponseClient + */ +public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { + + private OAuth2AccessTokenResponseClient accessTokenResponseClient = new DefaultTokenExchangeTokenResponseClient(); + + private Function subjectTokenResolver = this::resolveSubjectToken; + + private Function actorTokenResolver = (context) -> null; + + private Duration clockSkew = Duration.ofSeconds(60); + + private Clock clock = Clock.systemUTC(); + + /** + * Attempt to authorize (or re-authorize) the + * {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided + * {@code context}. Returns {@code null} if authorization (or re-authorization) is not + * supported, e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() + * authorization grant type} is not {@link AuthorizationGrantType#TOKEN_EXCHANGE + * token-exchange} OR the {@link OAuth2AuthorizedClient#getAccessToken() access token} + * is not expired. + * @param context the context that holds authorization-specific state for the client + * @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not + * supported + */ + @Override + @Nullable + public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + Assert.notNull(context, "context cannot be null"); + ClientRegistration clientRegistration = context.getClientRegistration(); + if (!AuthorizationGrantType.TOKEN_EXCHANGE.equals(clientRegistration.getAuthorizationGrantType())) { + return null; + } + OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); + if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) { + // If client is already authorized but access token is NOT expired than no + // need for re-authorization + return null; + } + if (authorizedClient != null && authorizedClient.getRefreshToken() != null) { + // If client is already authorized but access token is expired and a + // refresh token is available, delegate to refresh_token. + return null; + } + + OAuth2Token subjectToken = this.subjectTokenResolver.apply(context); + if (subjectToken == null) { + return null; + } + + OAuth2Token actorToken = this.actorTokenResolver.apply(context); + TokenExchangeGrantRequest grantRequest = new TokenExchangeGrantRequest(clientRegistration, subjectToken, + actorToken); + OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, grantRequest); + + return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), + tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); + } + + private OAuth2Token resolveSubjectToken(OAuth2AuthorizationContext context) { + if (context.getPrincipal().getPrincipal() instanceof OAuth2Token accessToken) { + return accessToken; + } + return null; + } + + private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration, + TokenExchangeGrantRequest tokenExchangeGrantRequest) { + try { + return this.accessTokenResponseClient.getTokenResponse(tokenExchangeGrantRequest); + } + catch (OAuth2AuthorizationException ex) { + throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex); + } + } + + private boolean hasTokenExpired(OAuth2Token token) { + return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + } + + /** + * Sets the client used when requesting an access token credential at the Token + * Endpoint for the {@code token-exchange} grant. + * @param accessTokenResponseClient the client used when requesting an access token + * credential at the Token Endpoint for the {@code token-exchange} grant + */ + public void setAccessTokenResponseClient( + OAuth2AccessTokenResponseClient accessTokenResponseClient) { + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + this.accessTokenResponseClient = accessTokenResponseClient; + } + + /** + * Sets the resolver used for resolving the {@link OAuth2Token subject token}. + * @param subjectTokenResolver the resolver used for resolving the {@link OAuth2Token + * subject token} + */ + public void setSubjectTokenResolver(Function subjectTokenResolver) { + Assert.notNull(subjectTokenResolver, "subjectTokenResolver cannot be null"); + this.subjectTokenResolver = subjectTokenResolver; + } + + /** + * Sets the resolver used for resolving the {@link OAuth2Token actor token}. + * @param actorTokenResolver the resolver used for resolving the {@link OAuth2Token + * actor token} + */ + public void setActorTokenResolver(Function actorTokenResolver) { + Assert.notNull(actorTokenResolver, "actorTokenResolver cannot be null"); + this.actorTokenResolver = actorTokenResolver; + } + + /** + * Sets the maximum acceptable clock skew, which is used when checking the + * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is + * 60 seconds. + * + *

+ * An access token is considered expired if + * {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. + * @param clockSkew the maximum acceptable clock skew + */ + public void setClockSkew(Duration clockSkew) { + Assert.notNull(clockSkew, "clockSkew cannot be null"); + Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0"); + this.clockSkew = clockSkew; + } + + /** + * Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access + * token expiry. + * @param clock the clock + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "clock cannot be null"); + this.clock = clock; + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultTokenExchangeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultTokenExchangeTokenResponseClient.java new file mode 100644 index 00000000000..19bd2be5720 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultTokenExchangeTokenResponseClient.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.endpoint; + +import java.util.Arrays; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +/** + * The default implementation of an {@link OAuth2AccessTokenResponseClient} for the + * {@link AuthorizationGrantType#TOKEN_EXCHANGE token-exchange} grant. This implementation + * uses a {@link RestOperations} when requesting an access token credential at the + * Authorization Server's Token Endpoint. + * + * @author Steve Riesenberg + * @since 6.3 + * @see OAuth2AccessTokenResponseClient + * @see TokenExchangeGrantRequest + * @see OAuth2AccessTokenResponse + * @see Section + * 2.1 Request + * @see Section + * 2.2 Response + */ +public final class DefaultTokenExchangeTokenResponseClient + implements OAuth2AccessTokenResponseClient { + + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + private Converter> requestEntityConverter = new ClientAuthenticationMethodValidatingRequestEntityConverter<>( + new TokenExchangeGrantRequestEntityConverter()); + + private RestOperations restOperations; + + public DefaultTokenExchangeTokenResponseClient() { + RestTemplate restTemplate = new RestTemplate( + Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); + restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + this.restOperations = restTemplate; + } + + @Override + public OAuth2AccessTokenResponse getTokenResponse(TokenExchangeGrantRequest tokenExchangeGrantRequest) { + Assert.notNull(tokenExchangeGrantRequest, "tokenExchangeGrantRequest cannot be null"); + RequestEntity requestEntity = this.requestEntityConverter.convert(tokenExchangeGrantRequest); + ResponseEntity responseEntity = getResponse(requestEntity); + + return responseEntity.getBody(); + } + + private ResponseEntity getResponse(RequestEntity requestEntity) { + try { + return this.restOperations.exchange(requestEntity, OAuth2AccessTokenResponse.class); + } + catch (RestClientException ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + + ex.getMessage(), + null); + throw new OAuth2AuthorizationException(oauth2Error, ex); + } + } + + /** + * Sets the {@link Converter} used for converting the + * {@link TokenExchangeGrantRequest} to a {@link RequestEntity} representation of the + * OAuth 2.0 Access Token Request. + * @param requestEntityConverter the {@link Converter} used for converting to a + * {@link RequestEntity} representation of the Access Token Request + */ + public void setRequestEntityConverter( + Converter> requestEntityConverter) { + Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null"); + this.requestEntityConverter = requestEntityConverter; + } + + /** + * Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token + * Response. + * + *

+ * NOTE: At a minimum, the supplied {@code restOperations} must be configured + * with the following: + *

    + *
  1. {@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and + * {@link OAuth2AccessTokenResponseHttpMessageConverter}
  2. + *
  3. {@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}
  4. + *
+ * @param restOperations the {@link RestOperations} used when requesting the Access + * Token Response + */ + public void setRestOperations(RestOperations restOperations) { + Assert.notNull(restOperations, "restOperations cannot be null"); + this.restOperations = restOperations; + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequest.java new file mode 100644 index 00000000000..ff835973308 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.endpoint; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.util.Assert; + +/** + * A Token Exchange Grant request that holds the {@link OAuth2Token subject token} and + * optional {@link OAuth2Token actor token}. + * + * @author Steve Riesenberg + * @since 6.3 + * @see AbstractOAuth2AuthorizationGrantRequest + * @see ClientRegistration + * @see OAuth2Token + * @see Section + * 1.1 Delegation vs. Impersonation Semantics + * @see Section + * 2.1 Request + * @see Section + * 2.2 Response + */ +public class TokenExchangeGrantRequest extends AbstractOAuth2AuthorizationGrantRequest { + + private final OAuth2Token subjectToken; + + private final OAuth2Token actorToken; + + /** + * Constructs a {@code JwtBearerGrantRequest} using the provided parameters. + * @param clientRegistration the client registration + * @param subjectToken the subject token + * @param actorToken the actor token + */ + public TokenExchangeGrantRequest(ClientRegistration clientRegistration, OAuth2Token subjectToken, + OAuth2Token actorToken) { + super(AuthorizationGrantType.TOKEN_EXCHANGE, clientRegistration); + Assert.notNull(subjectToken, "subjectToken cannot be null"); + this.subjectToken = subjectToken; + this.actorToken = actorToken; + } + + /** + * Returns the {@link OAuth2Token subject token}. + * @return the {@link OAuth2Token subject token} + */ + public OAuth2Token getSubjectToken() { + return this.subjectToken; + } + + /** + * Returns the {@link OAuth2Token actor token}. + * @return the {@link OAuth2Token actor token} + */ + public OAuth2Token getActorToken() { + return this.actorToken; + } + +} \ No newline at end of file diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequestEntityConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequestEntityConverter.java new file mode 100644 index 00000000000..da2edfb31f0 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequestEntityConverter.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.endpoint; + +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * An implementation of an {@link AbstractOAuth2AuthorizationGrantRequestEntityConverter} + * that converts the provided {@link TokenExchangeGrantRequest} to a {@link RequestEntity} + * representation of an OAuth 2.0 Access Token Request for the Token Exchange Grant. + * + * @author Steve Riesenberg + * @since 6.3 + * @see AbstractOAuth2AuthorizationGrantRequestEntityConverter + * @see TokenExchangeGrantRequest + * @see RequestEntity + * @see Section + * 1.1 Delegation vs. Impersonation Semantics + */ +public class TokenExchangeGrantRequestEntityConverter + extends AbstractOAuth2AuthorizationGrantRequestEntityConverter { + + private static final String ACCESS_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:access_token"; + + private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt"; + + @Override + protected MultiValueMap createParameters(TokenExchangeGrantRequest tokenExchangeGrantRequest) { + ClientRegistration clientRegistration = tokenExchangeGrantRequest.getClientRegistration(); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add(OAuth2ParameterNames.GRANT_TYPE, tokenExchangeGrantRequest.getGrantType().getValue()); + parameters.add(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE); + parameters.add(OAuth2ParameterNames.SUBJECT_TOKEN, tokenExchangeGrantRequest.getSubjectToken().getTokenValue()); + if (tokenExchangeGrantRequest.getSubjectToken() instanceof Jwt) { + parameters.add(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE); + } + else { + parameters.add(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE); + } + if (tokenExchangeGrantRequest.getActorToken() != null) { + parameters.add(OAuth2ParameterNames.ACTOR_TOKEN, tokenExchangeGrantRequest.getActorToken().getTokenValue()); + if (tokenExchangeGrantRequest.getActorToken() instanceof Jwt) { + parameters.add(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE); + } + else { + parameters.add(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE); + } + } + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + parameters.add(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) { + parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); + } + return parameters; + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java index 07b16e2b741..e1321bd7595 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java @@ -69,6 +69,12 @@ public final class AuthorizationGrantType implements Serializable { public static final AuthorizationGrantType DEVICE_CODE = new AuthorizationGrantType( "urn:ietf:params:oauth:grant-type:device_code"); + /** + * @since 6.3 + */ + public static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType( + "urn:ietf:params:oauth:grant-type:token-exchange"); + private final String value; /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index d387b482d94..41f63d9a23e 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -182,6 +182,42 @@ public final class OAuth2ParameterNames { */ public static final String INTERVAL = "interval"; + /** + * {@code requested_token_type} - used in Token Exchange Access Token Request. + * @since 6.3 + */ + public static final String REQUESTED_TOKEN_TYPE = "requested_token_type"; + + /** + * {@code issued_token_type} - used in Token Exchange Access Token Response. + * @since 6.3 + */ + private static final String ISSUED_TOKEN_TYPE = "issued_token_type"; + + /** + * {@code subject_token} - used in Token Exchange Access Token Request. + * @since 6.3 + */ + public static final String SUBJECT_TOKEN = "subject_token"; + + /** + * {@code subject_token_type} - used in Token Exchange Access Token Request. + * @since 6.3 + */ + public static final String SUBJECT_TOKEN_TYPE = "subject_token_type"; + + /** + * {@code actor_token} - used in Token Exchange Request. + * @since 6.3 + */ + public static final String ACTOR_TOKEN = "actor_token"; + + /** + * {@code actor_token_type} - used in Token Exchange Access Token Request. + * @since 6.3 + */ + public static final String ACTOR_TOKEN_TYPE = "actor_token_type"; + private OAuth2ParameterNames() { }