Describe the bug
TokenExchangeGrantRequest.tokenType() derives subject_token_type (and actor_token_type) from the Java type of the token:
private static String tokenType(OAuth2Token token) {
return (token instanceof Jwt) ? JWT_TOKEN_TYPE_VALUE : ACCESS_TOKEN_TYPE_VALUE;
}
OidcIdToken and Jwt are siblings, not parent and child — both extend AbstractOAuth2Token:
// oauth2-core
public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAccessor { ... }
// oauth2-jose (depends on oauth2-core)
public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor { ... }
An OidcIdToken is therefore an OAuth2Token but not a Jwt, so it takes the ACCESS_TOKEN_TYPE_VALUE branch. An ID token is labelled an access token, and there is no scenario in which that is the correct identifier.
RFC 8693 section 3 defines urn:ietf:params:oauth:token-type:access_token as indicating "an OAuth 2.0 access token issued by the given authorization server", and defines urn:ietf:params:oauth:token-type:id_token separately for an ID token.
This is not an oversight that a subclass would fix. OidcIdToken lives in oauth2-core and Jwt lives in oauth2-jose, which depends on oauth2-core; the dependency direction means OidcIdToken cannot extend Jwt. So instanceof Jwt has never been — and can never be — true for an ID token, even though an ID token is by definition a JWT.
The behaviour has been present since the grant was introduced in 6.3. The logic moved from TokenExchangeGrantRequestEntityConverter to TokenExchangeGrantRequest.defaultParameters() in 6.4; the method itself is unchanged. Confirmed on main.
To Reproduce
Public API only — no external project or custom resolver needed:
ClientRegistration registration = ClientRegistration.withRegistrationId("exchange")
.clientId("client")
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.tokenUri("https://example.com/oauth2/token")
.build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token").claim("sub", "user").build();
MultiValueMap<String, String> parameters =
new DefaultOAuth2TokenRequestParametersConverter<TokenExchangeGrantRequest>()
.convert(new TokenExchangeGrantRequest(registration, idToken, null));
parameters.getFirst("subject_token_type");
// -> "urn:ietf:params:oauth:token-type:access_token"
Expected behavior
subject_token_type should be urn:ietf:params:oauth:token-type:id_token.
How this is reached in practice
An OidcIdToken reaches the grant request through a custom subject token resolver — for example one returning oidcUser.getIdToken(). No Authentication shipped with Spring Security carries an OidcIdToken as its principal, so the default resolver never produces one.
That is the "Federated Identity" and "Backend-to-Backend with User Context" use cases raised in gh-19048 and acknowledged there as "solid use cases for id_token exchange" — in both, a Spring application is the client of the exchange and the subject token is an ID token. gh-19076 is implementing the server half now: an OidcIdTokenSubjectTokenResolver reading the IdP's JWKS URL from ClientSettings.idTokenJwkSetUrl(), whose first test scenario exchanges a valid ID token and expects an access token with the correct sub.
Relationship to #19436
#19436 asks that the RFC 8693 type identifiers be stated rather than inferred. That ask stands whatever happens here: no better mapping resolves any of its three defects — a mapping can only infer an identifier, and what is missing is the ability to state one.
This one is separable: gh-19436 stands without it, and a mapping change resolves this while leaving gh-19436's three defects untouched. And whatever resolver strategy eventually lands, tokenType() remains the default — an application that does not state an identifier still gets one from this method, and for an OidcIdToken it will still be wrong.
Mapping OidcIdToken to ...:id_token is not a behaviour change for anyone who is correct today, because labelling an ID token as an access token is never right.
I am happy to submit the PR.
Describe the bug
TokenExchangeGrantRequest.tokenType()derivessubject_token_type(andactor_token_type) from the Java type of the token:OidcIdTokenandJwtare siblings, not parent and child — both extendAbstractOAuth2Token:An
OidcIdTokenis therefore anOAuth2Tokenbut not aJwt, so it takes theACCESS_TOKEN_TYPE_VALUEbranch. An ID token is labelled an access token, and there is no scenario in which that is the correct identifier.RFC 8693 section 3 defines
urn:ietf:params:oauth:token-type:access_tokenas indicating "an OAuth 2.0 access token issued by the given authorization server", and definesurn:ietf:params:oauth:token-type:id_tokenseparately for an ID token.This is not an oversight that a subclass would fix.
OidcIdTokenlives inoauth2-coreandJwtlives inoauth2-jose, which depends onoauth2-core; the dependency direction meansOidcIdTokencannot extendJwt. Soinstanceof Jwthas never been — and can never be — true for an ID token, even though an ID token is by definition a JWT.The behaviour has been present since the grant was introduced in 6.3. The logic moved from
TokenExchangeGrantRequestEntityConvertertoTokenExchangeGrantRequest.defaultParameters()in 6.4; the method itself is unchanged. Confirmed onmain.To Reproduce
Public API only — no external project or custom resolver needed:
Expected behavior
subject_token_typeshould beurn:ietf:params:oauth:token-type:id_token.How this is reached in practice
An
OidcIdTokenreaches the grant request through a custom subject token resolver — for example one returningoidcUser.getIdToken(). NoAuthenticationshipped with Spring Security carries anOidcIdTokenas its principal, so the default resolver never produces one.That is the "Federated Identity" and "Backend-to-Backend with User Context" use cases raised in gh-19048 and acknowledged there as "solid use cases for
id_tokenexchange" — in both, a Spring application is the client of the exchange and the subject token is an ID token. gh-19076 is implementing the server half now: anOidcIdTokenSubjectTokenResolverreading the IdP's JWKS URL fromClientSettings.idTokenJwkSetUrl(), whose first test scenario exchanges a valid ID token and expects an access token with the correctsub.Relationship to #19436
#19436 asks that the RFC 8693 type identifiers be stated rather than inferred. That ask stands whatever happens here: no better mapping resolves any of its three defects — a mapping can only infer an identifier, and what is missing is the ability to state one.
This one is separable: gh-19436 stands without it, and a mapping change resolves this while leaving gh-19436's three defects untouched. And whatever resolver strategy eventually lands,
tokenType()remains the default — an application that does not state an identifier still gets one from this method, and for anOidcIdTokenit will still be wrong.Mapping
OidcIdTokento...:id_tokenis not a behaviour change for anyone who is correct today, because labelling an ID token as an access token is never right.I am happy to submit the PR.