Skip to content

Pr/OIDC issuer and password grant hardening - #4151

Open
fhanik wants to merge 4 commits into
developfrom
pr/oidc-issuer-and-password-grant-hardening
Open

fhanik wants to merge 4 commits into
developfrom
pr/oidc-issuer-and-password-grant-hardening

Conversation

@fhanik

@fhanik fhanik commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Two changes to how UAA handles tokens and errors around OIDC identity providers:

  1. An id_token that is verified against the zone's own token endpoint must carry the issuer of that zone.
  2. A rejection by an OIDC identity provider on the password grant is answered with the same generic error as wrong client credentials. A failure of the identity provider keeps a 500 and now carries the text of the identity
    provider.

Issuer of an id_token of the zone's own token endpoint

When an OIDC identity provider points at the token endpoint of the zone itself, the id_token is verified with the keys of the zone. A zone without keys of its own uses the keys of the default zone, so the signature alone does not
tell which zone issued the token.

ExternalOAuthAuthenticationManager.validateToken now chains checkIssuer(config.getIssuer()) on that branch. A token issued for another zone, or a token without an iss claim, is rejected.

This is the check that already applies to access and refresh tokens (TokenValidationService), to resolveOriginProvider, and to the other branch of validateToken. Tokens of the zone are unchanged, because they carry the
issuer of the zone. After the issuer.uri or the issuer of a zone is changed, tokens issued under the old value are no longer accepted on this path, as they are not after a rotation of the signing keys.

Errors of the identity provider on the OIDC password grant

PasswordGrantAuthenticationManager.oidcPasswordGrant:

Identity provider answers Before After
4xx 401, invalid_client, error_description is the whole body of the identity provider 401, invalid_client, error_description is Bad credentials, the same as for wrong client credentials. Status and body of the
identity provider are written to the log at DEBUG.
5xx 500 with an empty body 500, server_error, error_description is the text of the identity provider (the status text if the body is empty)

The error code stays invalid_client for a 4xx, so a caller cannot tell which part of the request failed. A 5xx stays visible as a 500.

The behavior is described in the documentation of the token endpoint (TokenEndpointDocs, on the grant_type parameter of the password grant).

Password grant and the audience of the id_token

A test documents that the password grant hands no origin to the external manager. The id_token is fetched by UAA from the identity provider itself, so it is not bound to the audience of a relying party again, and the identity
provider is resolved from the issuer of the token. This is the intended behavior and is unchanged.

Tests

Tests

  • ExternalOAuthAuthenticationManagerTest
    • getExternalAuthenticationDetails_whenUaaTokenHasAnotherIssuer_throws: a token signed with the key of the zone, bound to the relying party, but issued by another zone, is rejected.
    • verifySubjectToken_whenUaaTokenHasTheProviderIssuerAndAnotherAudience_stillVerifies: a token with the issuer of the provider and another audience still verifies.
  • PasswordGrantAuthenticationManagerTest: a 4xx gives Bad credentials; the body of the identity provider reaches the DEBUG log; a 5xx gives server_error with the text of the identity provider, or with the status text if the
    body is empty; no origin is handed to the external manager.
  • PasswordGrantIT, with a stand-in identity provider that answers every request with a chosen response:
    • a 400 with an internal host name in its body gives 401, invalid_client, Bad credentials, and the host name is not in the response;
    • a 503 with a text gives 500, server_error and that text.

…er of that zone

When an OIDC identity provider points at the token endpoint of the zone
itself, the id_token is verified with the keys of the zone. A zone without
keys of its own uses the keys of the default zone, so the signature alone
does not tell which zone issued the token.

The issuer of the token is now checked against the issuer of the identity
provider. This is the check that access and refresh tokens, and the other
branch of this method, already make. A token issued for another zone, or
a token without an iss claim, is rejected. Tokens of the zone are
unchanged, because they carry the issuer of the zone.
… grant with the generic bad credentials error

When the identity provider of a password grant rejects the request with a
4xx response, the whole body of that response was returned to the caller
as the error_description. The caller now receives the same answer as for
wrong client credentials: 401, invalid_client, "Bad credentials". The
status and the body of the identity provider are written to the log at
DEBUG.

When the identity provider fails with a 5xx response, the caller receives
500 with the error server_error and the text of the identity provider as
the error_description. The body of that response was empty before.

The behavior is described in the documentation of the token endpoint.
The id_token of a password grant is fetched by UAA from the identity
provider itself, so it is not bound to the audience of a relying party
again, and the identity provider is resolved from the issuer of the token.
This is intended. The test documents it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Token endpoint documentation is incomplete because it omits that UAA exposes the HTTP status text when the provider returns an empty error body.

1 open issue
What changed in this PR

This pull request hardens OIDC token validation and standardizes password-grant error handling for external identity providers.

Changes:

  • Requires self-issued UAA ID tokens to contain the current zone’s issuer.
  • Maps provider 4xx responses to generic invalid_client/Bad credentials errors and provider 5xx responses to server_error.
  • Adds unit, integration, and token endpoint documentation coverage.
File Description
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​login/​TokenEndpointDocs.java Documents OIDC provider error responses for password grants.
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​integration/​feature/​PasswordGrantIT.java Adds end-to-end coverage for provider rejection and failure responses.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​ExternalOAuthAuthenticationManagerTest.java Tests issuer enforcement and password-grant audience behavior.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​authentication/​manager/​PasswordGrantAuthenticationManagerTest.java Tests provider error mapping, logging, fallback status text, and origin handling.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​ExternalOAuthAuthenticationManager.java Adds issuer validation for self-referencing UAA OIDC providers.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​authentication/​manager/​PasswordGrantAuthenticationManager.java Normalizes provider 4xx errors and maps provider 5xx failures to OAuth server errors.

Review effort: Lite

…back for empty provider error bodies'

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain, with coverage added for issuer enforcement and the updated OAuth error handling across unit, integration, and documentation tests.

No open issues 🎉

1 resolved since last review

Review effort: Lite

@github-project-automation github-project-automation Bot moved this from Inbox to Pending Merge | Prioritized in Foundational Infrastructure Working Group Nov 12, 2026
throw new BadCredentialsException("Bad credentials", e);
} catch (HttpServerErrorException e) {
// a failure of the provider itself is passed on to the client, with the text of the provider
String description = StringUtils.hasText(e.getResponseBodyAsString()) ? e.getResponseBodyAsString() : e.getStatusText();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we return a generic server error here? The provider’s response body can contain internal diagnostics that would be exposed to the password grant client. For example, a provider error containing Database connection failed: db.internal.example:5432 would be returned in error_description.

This branch has not been deployed

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

Labels

Projects

Status: Pending Merge | Prioritized

Development

Successfully merging this pull request may close these issues.

4 participants