Enforce audience check for self-referencing OIDC identity providers - #4055
Conversation
An IdP registered with an issuer equal to the current zone's own token endpoint skipped audience validation on the presented id_token, since that check was only wired up for external OIDC providers. Any UAA-signed JWT for any client, not just the self-referencing IdP's own relying party, was accepted as a valid id_token for that login, letting a token issued in one client context be replayed to authenticate a different external-OIDC-mapped identity. Now the relying party's client ID is checked against the token's aud claim whenever one is configured, matching the validation already done for external providers. The internal id_token exchange used by the JWT bearer grant is unaffected, since it never sets a relying party ID.
There was a problem hiding this comment.
Pull request overview
This PR closes a security gap in the OIDC external authentication flow where a self-referencing OIDC IdP (issuer matches the current zone’s token endpoint) could accept any UAA-signed JWT as an id_token without ensuring it was issued for the IdP’s configured relying party. The change aligns self-referencing IdP validation with the audience-binding behavior already applied to external OIDC providers.
Changes:
- Enforce
aud(audience) validation for self-referencing OIDC IdPs when a relying party client ID is configured. - Add integration tests proving rejection of UAA-signed tokens with the wrong
audand with noaudclaim in the self-referencing IdP scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java |
Adds audience enforcement for self-referencing OIDC IdPs to prevent accepting UAA-signed tokens issued to other clients. |
server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java |
Adds regression tests covering wrong/missing audience for self-referencing OIDC IdP id_token validation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (hasLength(config.getRelyingPartyId())) { | ||
| // an explicitly registered self-referencing OIDC IdP must still bind the id_token | ||
| // to its own relying party, otherwise any token signed by this UAA instance | ||
| // (issued to any client, for any purpose) would be accepted as this IdP's id_token | ||
| jwtToken.checkAudience(config.getRelyingPartyId()); | ||
| } |
| // e.g. a plain access token response with no id_token/aud at all, as opposed to a real id_token | ||
| claims.put("sub", RandomStringUtils.random(50)); |
…check
The new audience check on self-referencing OIDC IdPs applied even when
the id_token arrived through the JWT Bearer grant or the password grant's
id_token exchange, both of which intentionally chain UAA-signed tokens
minted for other clients in the same zone. Those flows already
authenticate the calling client directly to /oauth/token, so binding the
presented token to the IdP's relying party doesn't apply the way it does
for an interactive browser login.
Only enforce the audience check when the caller supplied an explicit
origin, which is how the interactive callback (/login/callback/{origin})
always invokes this path; token-exchange callers omit the origin and let
it get resolved from the token's own issuer claim instead. Also swap
hasLength for hasText when checking whether a relying party ID is
configured, so a whitespace-only value doesn't count as configured.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java:273
- The comment about the origin-omitted flow reads like it has a dangling/duplicated fragment ("minted for other clients…") and is a bit hard to follow. It would be clearer to rewrite it as a single coherent explanation of why relying-party audience binding is skipped only for self-referencing tokens when origin is omitted.
// When origin is omitted (JWT Bearer/password-grant token exchange), we still validate audience for
// external IdPs, but skip the relying-party audience binding for self-referencing (UAA-issued) tokens
// to allow the intended token-chaining behavior.
// minted for other clients in the same zone/origin - so it is exempt from that binding.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java:273
- This comment block has a dangling/fragmented sentence ("minted for other clients…") and reads inconsistently. Rewording it into a single coherent explanation will make the audience-binding behavior easier to understand and avoid misleading future changes.
// bind the presented id_token to that IdP's relying party (audience).
// When origin is omitted (JWT Bearer/password-grant token exchange), we still validate audience for
// external IdPs, but skip the relying-party audience binding for self-referencing (UAA-issued) tokens
// to allow the intended token-chaining behavior.
// minted for other clients in the same zone/origin - so it is exempt from that binding.
|
|
||
| if (tokenEndpointBuilder.getTokenEndpoint(identityZoneManager.getCurrentIdentityZone()).equals(config.getIssuer())) { | ||
| List<SignatureVerifier> signatureVerifiers = getTokenKeyForUaaOrigin(); | ||
| jwtToken = buildIdTokenValidator(idToken, new ChainedSignatureVerifier(signatureVerifiers), keyInfoService); |
An IdP registered with an issuer equal to the current zone's own token
endpoint skipped audience validation on the presented id_token, since
that check was only wired up for external OIDC providers. Any UAA-signed
JWT for any client, not just the self-referencing IdP's own relying
party, was accepted as a valid id_token for that login, letting a token
issued in one client context be replayed to authenticate a different
external-OIDC-mapped identity.
Now the relying party's client ID is checked against the token's aud
claim whenever one is configured, matching the validation already done
for external providers. The internal id_token exchange used by the JWT
bearer grant is unaffected, since it never sets a relying party ID.