Skip to content

Add ID token support for token exchange#19076

Open
bkoragan wants to merge 1 commit intospring-projects:mainfrom
bkoragan:gh-19048
Open

Add ID token support for token exchange#19076
bkoragan wants to merge 1 commit intospring-projects:mainfrom
bkoragan:gh-19048

Conversation

@bkoragan
Copy link
Copy Markdown

I discussed this approach and received positive feedback. This PR implements the proposed design.

Summary

Add support for exchanging externally-issued OIDC ID tokens for access tokens via the OAuth 2.0 Token Exchange Grant per RFC 8693 - sec 3.

New classes:

  • OAuth2TokenExchangeSubjectTokenResolver — strategy interface for resolving external subject tokens
  • OAuth2TokenExchangeSubjectTokenContext — context object with resolved principal, claims, and scopes
  • OidcIdTokenSubjectTokenResolver — default implementation using JwtDecoderFactory<RegisteredClient>

Modified classes:

  • OAuth2TokenExchangeAuthenticationConverter — acceptid_token as supported token type
  • OAuth2TokenExchangeAuthenticationProvider — delegate to resolver before falling back to authorization service
  • OAuth2TokenEndpointConfigurer — auto-wire resolver bean

Design Decisions

  • Provider stays final; only token resolution is pluggable
  • Resolver returns null for unsupported types (chain-of-responsibility, same pattern as AuthenticationConverter)
  • JwtDecoderFactory<RegisteredClient> follows the same pattern as JwtClientAssertionDecoderFactory
  • Bean auto-wiring via getOptionalBean (same as OAuth2AuthorizationService, OAuth2TokenGenerator`)
  • No breaking changes; existing behavior untouched when no resolver is configured.

Configuration Example

@Bean
OidcIdTokenSubjectTokenResolver subjectTokenResolver() {
    return new OidcIdTokenSubjectTokenResolver(
        (registeredClient) -> {
            String jwkSetUri = registeredClient
                .getClientSettings()
                .getSetting("id-token-jwk-set-uri");
            return NimbusJwtDecoder
                .withJwkSetUri(jwkSetUri).build();
        }
    );
}

**Scenarios tested - as standard alone:**

| # | Scenario | Result |
|---|----------|--------|
| 1 | Exchange valid ID token (self-signed) | 200 + access token with correct `sub` |
| 2 | Tampered/invalid ID token | 400 `invalid_grant` |
| 3 | Expired ID token | 400 `invalid_grant` |
| 4 | ID token without `sub` claim | 400 `invalid_grant` |
| 5 | Unsupported token type | 400 `unsupported_token_type` |
| 6 | Resolver returns null, fallback to authorizationService | Works as before |
| 7 | Client credentials grant (backward compatabele) | 200 (no change) |
| 8 | Multi-IdP routing (GitHub, Google issuers) | Correct issuer-based decoder selection |

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 15, 2026
Introduce support for exchanging externally-issued OIDC
ID tokens for access tokens via the OAuth 2.0 Token
Exchange Grant per RFC 8693 Section 3.

- Add urn:ietf:params:oauth:token-type:id_token as a
  supported token type in the converter
- Add OAuth2TokenExchangeSubjectTokenResolver strategy
  interface for resolving external subject tokens
- Add OidcIdTokenSubjectTokenResolver as the default
  implementation using JwtDecoderFactory
- Modify OAuth2TokenExchangeAuthenticationProvider to
  delegate to the resolver before falling back to the
  authorization service
- Auto-wire the resolver bean in the configurer

Closes spring-projectsgh-19048

Signed-off-by: Bapuji Koraganti <bapuk.2008@gmail.com>
@bkoragan
Copy link
Copy Markdown
Author

@jgrandja Please review and share if any feedback. Note: reference to our proposal discussion :#19048 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants