Skip to content

Harden the RFC 8705 mTLS token endpoint - Supersedes #3972 - #4076

Closed
fhanik wants to merge 107 commits into
cloudfoundry:developfrom
fhanik:review/pr3792-fix
Closed

fhanik wants to merge 107 commits into
cloudfoundry:developfrom
fhanik:review/pr3792-fix

Conversation

@fhanik

@fhanik fhanik commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

RFC 8705 mutual-TLS client authentication (tls_client_auth), hardened

Supersedes #3972. Adds tls_client_auth client authentication at a dedicated POST /oauth/mtls/token endpoint, with
optional certificate-bound access tokens (cnf.x5t#S256), per-client trust configuration, named per-zone trust
bundles, Gorouter/XFCC support and optional RFC 8707 resource indicators.

Off by default, opt-in per zone. Nothing changes for a deployment that does not set uaa.mtls.enabled: true, and
a tenant zone only gets mTLS once it sets config.mtls.enabled: true.

Merge order: this PR must merge after #4107, the separate granted_scopes refresh fix. The mTLS token
enhancer is always registered, so with mTLS on
the refresh-claim copy runs and would otherwise copy granted_scopes onto narrowed refreshed access tokens.

Enablement model

Switch Default Scope Changed by
uaa.mtls.enabled false Deployment. Reconfigures the Tomcat connector, so it is read at startup. uaa.yml, restart
config.mtls.enabled false One identity zone. The default zone's value is reapplied from uaa.mtls.enabled on every restart. identity-zone API, no restart

mTLS is effective in a zone only when both are on, evaluated per request. A zone cannot override a disabled
deployment, and disabling the default zone does not affect tenant zones (and vice versa). All mTLS beans and the
security chain always exist; availability is enforced at runtime (MtlsFeaturePolicy, MtlsZoneAvailabilityFilter).

What the feature does

  • Exclusive mTLS clients. A client with tls-client-auth-ca (inline PEM) or tls-client-auth-ca-ref (named
    bundle) authenticates only at /oauth/mtls/token, only with a certificate, and is refused at /oauth/token.
    /oauth/mtls/token in turn refuses clients without that configuration, so it is not an alias of /oauth/token.
  • RFC 8705 section 2.1.2 subject binding. Exactly one of tls_client_auth_subject_dn, tls_client_auth_san_dns,
    …_san_uri, …_san_ip (address literal, compared in binary, never resolved through DNS) or …_san_email is
    required. Chain validation alone would let any holder of a certificate from a shared CA (Diego instance identity)
    authenticate as any client trusting it.
  • Trust. PKIX validation against the client's CA. Every PEM in a bundle is a trust anchor, so CA rotation works
    without downtime. Expired leaves and CA certificates used as the leaf are refused. Revocation is not checked.
  • Named trust bundles. uaa.mtls.trust_bundles for the default zone, config.mtls.trustBundles for other
    zones. Names and contents are isolated per zone. GET /oauth/mtls/trust-bundles lists names only.
  • Gorouter / XFCC. tls-client-auth-trusted-proxy-ca(-ref) makes a client proxy-only. The genuine TLS peer must
    chain to that CA and match the mandatory tls-client-auth-trusted-proxy-subject-dn before the
    X-Forwarded-Client-Cert header is trusted. Without proxy trust the header is never consulted.
  • Tokens. Only client_credentials, no refresh token. cnf.x5t#S256 only when the client sets
    tls_client_certificate_bound_access_tokens: true (RFC 8705 section 3.4); otherwise an ordinary bearer JWT. Claim
    mappings from subject_cn / subject_ou / subject_o (bounded regex on OU only), sub/aud templates, required
    claims. Reserved and delegation claims (amr, acr, cnf, act, may_act, dotted children) cannot be mapped.
  • RFC 8707 resource indicators. At most one resource per request, from the client's
    tls-client-auth-allowed-resources; anything else is invalid_target.
  • Endpoint shape. POST only (405 + Allow: POST otherwise); anything below the path is 404, which closes the
    /oauth/mtls/token/oauth/token routing bypass.
  • Discovery. With mTLS effective in the zone: tls_client_auth in token_endpoint_auth_methods_supported,
    mtls_endpoint_aliases.token_endpoint and tls_client_certificate_bound_access_tokens: true.

With mTLS off — no cost, no behaviour change

This was audited path by path for uaa.mtls.enabled=false with no zone configuration.

  • Request path. The new filters only compare the servlet path. No added database or zone lookups, regex work or
    certificate parsing on non-mTLS requests. The inactive token enhancer is skipped, so token issuance takes the same
    path as on develop.
  • Startup. The Tomcat customizer returns before touching the connector. The buildpack ClientCertificateMapper
    class is never loaded (its registration is disabled around a pass-through), and the jar registers no
    auto-configuration (guarded by a test). The trust-bundle startup validator returns immediately. No Flyway migrations.
  • Startup scale. Even with mTLS on, startup inspects only the default zone's own bundle definitions. It never
    enumerates zones or clients, so it does not grow with tens of thousands of zones. Client bundle references are
    checked at registration and resolved per request; an unresolvable reference fails closed at token time.
  • Observable output. /oauth/mtls/token and /oauth/mtls/trust-bundles answer 404 (both are new paths).
    Discovery is byte-for-byte develop's document (the serialized fixture is unchanged). Zone JSON omits config.mtls
    until a zone configures it, and a null value reads as the default.
  • Rolling upgrade. UaaAuthenticationDetails (stored in Spring Session) gained a field; its serialVersionUID
    is pinned to develop's value, and a test deserializes an instance serialized by develop.
  • Public API. ClientAuthentication behaves exactly as on develop apart from the new TLS_CLIENT_AUTH constant;
    UAA_SUPPORTED_METHODS, which governs authentication to external IdPs, does not include tls_client_auth.
    ClientAdminEndpointsValidator, ClientAdminBootstrap and ZoneEndpointsClientDetailsValidator keep their develop
    constructors; ClientDetailsAuthenticationProvider only gains an overload.

Compatibility notes

Change Who is affected
mTLS client configuration is validated on every client create, update and oauth.clients bootstrap, whether or not mTLS is on: valid configuration is stored, malformed configuration is a 400 (or fails bootstrap) only clients that carry the new tls-client-auth-* / tls_client_* keys
A stored client with tls-client-auth-ca(-ref) or proxy trust is mTLS-only: no secret fallback, even with mTLS off; a malformed or null value fails closed only clients carrying these keys, which UAA did not define before
A stored client with an unparseable tls_client_certificate_bound_access_tokens value cannot get a token same
jwt.token.claims.exclude cannot remove cnf (it would silently turn a bound token into a bearer token) only deployments that list cnf, which UAA never issued before
UaaTokenEnhancer gains default methods getLateOverrideClaims() (empty set) and isEnabled() (true) none; existing implementations compile and behave unchanged
ClientAdminBootstrap now runs after the identity-zone bootstrap, so default-zone bundles exist when clients are validated none

Operational notes

  • Enabling uaa.mtls.enabled is connector-wide: every TLS handshake gets a CertificateRequest with no issuer
    list (certificateVerification=optionalNoCA), and the connector moves to the FIPS BouncyCastle JSSE provider
    (required for TLS 1.3 post-handshake client authentication). Soak-test interop and performance first.
  • UAA stamps cnf but does not enforce it; resource servers must compare it with the certificate they see.
    /introspect and /check_token return cnf for JWT and opaque tokens.
  • Not implemented: revocation checking, audit events for mTLS issuance, a dedicated rate-limit mapping for
    /oauth/mtls/token, self_signed_tls_client_auth, and certificate binding for public clients.

Where to look

Area Files
Authentication ClientDetailsAuthenticationProvider, TlsClientAuthentication, TlsClientAuthSubjectMatcher, OuClaimPattern, UaaAuthenticationDetails
Endpoint and filters UaaTokenEndpoint, MtlsEndpointAvailabilityFilter, MtlsZoneAvailabilityFilter, RawPeerCertificateCaptureFilter, MtlsPathGuardedFilter, SpringServletXmlFiltersConfiguration, SpringServletXmlSecurityConfiguration, OauthEndpointSecurityConfiguration, FilterChainOrder
Token issuance MtlsClaimsEnhancer, UaaTokenServices, UaaTokenEnhancer, ClientCredentialsTokenGranter
Enablement and trust bundles MtlsFeaturePolicy, MtlsConfig, IdentityZoneConfiguration, IdentityZoneConfigurationBootstrap, GeneralIdentityZoneConfigurationValidator, MtlsTrustBundles, MtlsTrustBundleEndpoints, MtlsTrustBundleStartupValidator, ClientAdminSecurityConfiguration
Registration ClientAdminEndpointsValidator, ZoneEndpointsClientDetailsValidator, ClientAdminBootstrap, TlsClientAuthConfiguration, UaaClientDetails
Transport MtlsClientAuthTomcatCustomizer, BCJSSEUtil, BCJSSESSLContext, BCJSSESslImplementation, NoAcceptedIssuersTrustManager
Discovery and model OpenIdConnectEndpoints, OpenIdConfiguration, ClientAuthentication, TokenConstants, OAuth2Exception, InvalidTargetException
Build java-buildpack-client-certificate-mapper-jakarta 2.0.1 and spring-boot-tomcat in libs.versions.toml / server/build.gradle.kts; bouncyCastlePkixFips for tests

Tests

About 450 new test methods: 24 new test classes (MockMvc suites for hardening, zone paths, flag-off behaviour and flag
spelling, a real embedded-Tomcat TLS integration test, and unit tests for every new class) plus 124 tests added to 25
existing classes. Both directions of the zone policy are covered (default zone off with a tenant on, and the reverse).
No pre-existing test was deleted or disabled. Changes to existing assertions: UaaClientDetailsTest asserts
hashCode() is non-zero rather than positive; TokenEndpointDocs runs with uaa.mtls.enabled=true for its mTLS
example; OpenIdConnectEndpointDocs documents the two optional discovery fields. The rest are constructor and wiring
updates.

./gradlew clean test integrationTest passes.

Documentation

docs/UAA-Client-Authentication.md, docs/UAA-Configuration-Reference.md (uaa.mtls.enabled,
uaa.mtls.trust_bundles), the new docs/UAA-mTLS-Getting-Started.md, the generated API docs (token endpoint,
discovery, introspection and check-token cnf, client registration keys, identity-zone config.mtls, trust-bundle
names) and the slate source.

🤖 Generated with Claude Code

@fhanik fhanik mentioned this pull request Sep 16, 2026
@fhanik fhanik changed the title Example fixes for 3792 Harden the RFC 8705 mTLS token endpoint - Supersedes 3792 Sep 24, 2026
@fhanik
fhanik force-pushed the review/pr3792-fix branch 2 times, most recently from a3235f7 to 6d0bf7a Compare September 24, 2026 13:48
@fhanik
fhanik marked this pull request as ready for review September 24, 2026 13:48
@strehle
strehle requested a lite review from Copilot September 24, 2026 14:06

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.

Copilot review overview

🟡 Changes recommended

Unresolved critical endpoint-path rejection and moderate dotted reserved-claim protection gaps remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

Adds and hardens RFC 8705 mTLS token-endpoint support, including authentication, claim protections, endpoint gating, TLS integration, discovery metadata, documentation, and tests.

Changes:

  • Adds certificate-based client authentication, client_credentials restrictions, disabled-endpoint handling, and protected claim mapping.
  • Adds FIPS BCJSSE/Tomcat support, discovery metadata, configuration/API documentation, and extensive tests.
  • Final findings: a critical regular-endpoint rejection and moderate dotted reserved-claim validation gaps remain.
File Reviewed change
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​scim/​endpoints/​OpenIdConnectEndpointsMockMvcZonePathTests.java Tests zone-path discovery metadata.
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​scim/​endpoints/​OpenIdConnectEndpointsMockMvcTests.java Tests mTLS discovery metadata.
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​scim/​endpoints/​OpenIdConnectEndpointDocs.java Documents discovery aliases.
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​UaaTokenServicesTests.java Tests claim protection and overrides.
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​mock/​token/​MtlsDisabledTokenEndpointMockMvcTests.java Tests disabled mTLS behavior.
uaa/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​login/​TokenEndpointDocs.java Documents mTLS token requests.
uaa/​slateCustomizations/​source/​index.html.md.erb Adds mTLS API documentation.
uaa/​build.gradle.kts Adds certificate test dependencies.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​MtlsClientAuthTomcatCustomizerTest.java Tests Tomcat mTLS customization.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​BCJSSEUtilTest.java Tests BCJSSE protocols.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​BCJSSESSLContextTest.java Tests BCJSSE contexts.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​OauthIdentityProviderDefinitionFactoryBeanTest.java Tests unsupported TLS authentication rejection.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​ExternalOAuthIdentityProviderConfigValidatorTest.java Tests external OAuth validation.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​ExternalOAuthAuthenticationManagerTest.java Tests stale TLS configuration rejection.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​token/​UaaTokenEndpointTests.java Tests mTLS endpoint mappings.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​RawPeerCertificateCaptureFilterTest.java Tests peer certificate capture.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​RawPeerCertificateCaptureFilterRegistrationTest.java Tests filter registration and ordering.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​ClientCertificateMapperFilterTest.java Tests certificate mapper behavior.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​provider/​client/​ClientCredentialsTokenGranterTests.java Tests TLS client-credentials grants.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​beans/​OauthEndpointSecurityConfigurationTests.java Tests conditional mTLS security wiring.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​client/​ClientAdminEndpointsTests.java Updates validator construction.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​client/​ClientAdminBootstrapTests.java Tests bootstrap mTLS validation.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​client/​ClientAdminBootstrapMultipleSecretsUpdateTests.java Updates bootstrap fixtures.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​client/​ClientAdminBootstrapMultipleSecretsTest.java Updates bootstrap fixtures.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​authentication/​UaaClientAuthenticationProviderTest.java Tests TLS client authentication rejection.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​authentication/​ClientDetailsAuthenticationProviderTests.java Tests certificate authentication and required claims.
server/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​account/​OpenIdConnectEndpointsTest.java Tests conditional discovery metadata.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​zone/​ZoneEndpointsClientDetailsValidator.java Validates zone client mTLS configuration.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​NoAcceptedIssuersTrustManager.java Provides TLS trust-manager handling.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​MtlsClientAuthTomcatCustomizer.java Configures mTLS connectors.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​BCJSSEUtil.java Integrates BCJSSE with Tomcat.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​BCJSSESslImplementation.java Selects BCJSSE implementation.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​web/​tomcat/​BCJSSESSLContext.java Wraps BCJSSE SSL contexts.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​web/​FilterChainOrder.java Adds mTLS filter ordering.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​SpringServletXmlFiltersConfiguration.java Registers mTLS filters.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​SpringServletXmlBeansConfiguration.java Wires mTLS configuration.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​OauthIDPWrapperFactoryBean.java Excludes TLS authentication from external IdPs.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​ExternalOAuthIdentityProviderConfigValidator.java Validates external OAuth methods.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​provider/​oauth/​ExternalOAuthAuthenticationManager.java Rejects unsupported TLS authentication.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​UaaTokenServices.java Protects UAA-owned JWT claims.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​token/​UaaTokenEndpoint.java Restricts mTLS grants.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​RawPeerCertificateCaptureFilter.java Captures genuine peer certificates.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​MtlsPathGuardedFilter.java Scopes certificate mapping to mTLS paths.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​MtlsEndpointAvailabilityFilter.java Hides the disabled mTLS endpoint.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​tls/​MtlsClaimsEnhancer.java Adds certificate-derived claims; dotted reserved parents remain unresolved.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​provider/​client/​ClientCredentialsTokenGranter.java Allows TLS client authentication.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​beans/​OauthEndpointSecurityConfiguration.java Adds the mTLS security chain.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​beans/​OauthEndpointBeanConfiguration.java Wires TLS authentication.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​client/​ClientAdminBootstrap.java Validates bootstrapped mTLS clients.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​authentication/​ClientDetailsAuthenticationProvider.java Enforces mTLS authentication; regular /oauth/token handling has a critical rejection issue.
server/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​account/​OpenIdConnectEndpoints.java Advertises mTLS discovery metadata.
server/​src/​integrationTest/​java/​org/​cloudfoundry/​identity/​uaa/​client/​ClientAdminBootstrapProdEncoderTest.java Updates bootstrap integration coverage.
server/​build.gradle.kts Adds Tomcat and certificate-mapper dependencies.
model/​src/​test/​resources/​org/​cloudfoundry/​identity/​uaa/​account/​OpenIdConfiguration.json Updates discovery fixture.
model/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​constants/​ClientAuthenticationTest.java Tests TLS authentication constants.
model/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​client/​UaaClientDetailsTest.java Tests TLS configuration persistence.
model/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​client/​TlsClientAuthConfigurationTest.java Tests TLS configuration serialization.
model/​src/​test/​java/​org/​cloudfoundry/​identity/​uaa/​account/​OpenIdConfigurationTests.java Tests conditional discovery fields.
model/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​oauth/​token/​TokenConstants.java Adds the TLS authentication constant.
model/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​constants/​ClientAuthentication.java Adds TLS authentication support.
model/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​client/​UaaClientDetails.java Stores TLS client configuration.
model/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​client/​TlsClientAuthConfiguration.java Defines TLS configuration and protected claims.
model/​src/​main/​java/​org/​cloudfoundry/​identity/​uaa/​account/​OpenIdConfiguration.java Adds mTLS discovery fields.
gradle/​libs.versions.toml Adds dependency aliases and versions.
docs/​UAA-Configuration-Reference.md Documents the mTLS feature flag.
docs/​UAA-Client-Authentication.md Documents RFC 8705 client authentication.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

Copilot review overview

🔵 Needs a closer look

Unresolved moderate issues affect reserved-claim protection, client equality/hash semantics, and malformed configuration handling.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)

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.

Copilot review overview

🟡 Changes recommended

The mapper dependency can auto-register an earlier unguarded filter that defeats genuine peer-certificate capture, and configuration validation/documentation also have unresolved gaps.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
Resolved since last review (3)

Comment thread server/build.gradle.kts
Comment thread docs/UAA-Client-Authentication.md Outdated
@strehle
strehle requested a balanced review from Copilot September 24, 2026 18:55

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

fhanik added a commit to fhanik/uaa that referenced this pull request Sep 25, 2026
…, docs

Three findings from the latest Copilot review of PR cloudfoundry#4076
(review 5306465064), verified against the code before implementing:

1. HIGH - "Prevent competing certificate mapper from overwriting the
   peer certificate" (server/build.gradle.kts:44). Verified by
   decompiling the java-buildpack-client-certificate-mapper-jakarta
   jar: it ships a @ConditionalOnCloudPlatform(CLOUD_FOUNDRY)
   @configuration class, ClientCertificateMapperAutoConfiguration, that
   registers its own ClientCertificateMapper filter at
   Ordered.HIGHEST_PRECEDENCE -- ahead of RawPeerCertificateCaptureFilter
   (-300). If ever activated, it would overwrite the servlet
   X509Certificate attribute with XFCC data before the genuine TLS peer
   certificate is captured, defeating direct-connection mTLS
   authentication. The jar carries no spring.factories /
   AutoConfiguration.imports entry naming it, so nothing triggers this
   today, but a routine dependency bump could add one with no code-level
   signal. UaaBootApplication now excludes it by name (excludeName,
   since the jar is implementation-scoped in the server module and not
   on uaa's compile classpath) as permanent, version-independent
   insurance.

2. MEDIUM - "Reject unsupported subject patterns and captureless OU
   patterns" (ClientAdminEndpointsValidator.java:533). Verified against
   TlsClientAuthentication.extractClaimMappingValues: pattern is applied
   only when field == "subject_ou" (the subject_cn/subject_o switch arms
   ignore mapping.getPattern() entirely), and matchFirstOu only returns
   a value via m.group(1), guarded by m.groupCount() >= 1. The validator
   accepted both shapes as syntactically valid, so a client config could
   be registered that would never produce the claim it declares.
   validateTlsClientAuthClaimConfig now rejects a pattern on
   subject_cn/subject_o and a subject_ou pattern with no capturing
   group.

3. LOW - "Document template length, placeholder, and mapping
   constraints" (docs/UAA-Client-Authentication.md:180). The
   tls-client-auth-claim-mappings / -sub-template / -aud-templates rows
   didn't mention the validator's enforced constraints (256-character
   cap, at least one {claim} placeholder, placeholder must name a
   declared claim, plus the pattern constraints from #2). Documented.

Two existing tests (validateTlsClientAuthClaimConfig_acceptsValid
NativeClaimMappings / _acceptsValidJsonStringClaimMappings) used a
pattern on subject_cn -- exactly the now-rejected shape -- and were
updated to use subject_ou instead, which still exercises the pattern
path meaningfully.

Full ./gradlew clean test passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 25, 2026
…, docs

Three findings from the latest Copilot review of PR cloudfoundry#4076
(review 5306465064), verified against the code before implementing:

1. HIGH - "Prevent competing certificate mapper from overwriting the
   peer certificate" (server/build.gradle.kts:44). Verified by
   decompiling the java-buildpack-client-certificate-mapper-jakarta
   jar: it ships a @ConditionalOnCloudPlatform(CLOUD_FOUNDRY)
   @configuration class, ClientCertificateMapperAutoConfiguration, that
   registers its own ClientCertificateMapper filter at
   Ordered.HIGHEST_PRECEDENCE -- ahead of RawPeerCertificateCaptureFilter
   (-300). If ever activated, it would overwrite the servlet
   X509Certificate attribute with XFCC data before the genuine TLS peer
   certificate is captured, defeating direct-connection mTLS
   authentication. The jar carries no spring.factories /
   AutoConfiguration.imports entry naming it, so nothing triggers this
   today, but a routine dependency bump could add one with no code-level
   signal. UaaBootApplication now excludes it by name (excludeName,
   since the jar is implementation-scoped in the server module and not
   on uaa's compile classpath) as permanent, version-independent
   insurance.

2. MEDIUM - "Reject unsupported subject patterns and captureless OU
   patterns" (ClientAdminEndpointsValidator.java:533). Verified against
   TlsClientAuthentication.extractClaimMappingValues: pattern is applied
   only when field == "subject_ou" (the subject_cn/subject_o switch arms
   ignore mapping.getPattern() entirely), and matchFirstOu only returns
   a value via m.group(1), guarded by m.groupCount() >= 1. The validator
   accepted both shapes as syntactically valid, so a client config could
   be registered that would never produce the claim it declares.
   validateTlsClientAuthClaimConfig now rejects a pattern on
   subject_cn/subject_o and a subject_ou pattern with no capturing
   group.

3. LOW - "Document template length, placeholder, and mapping
   constraints" (docs/UAA-Client-Authentication.md:180). The
   tls-client-auth-claim-mappings / -sub-template / -aud-templates rows
   didn't mention the validator's enforced constraints (256-character
   cap, at least one {claim} placeholder, placeholder must name a
   declared claim, plus the pattern constraints from #2). Documented.

Two existing tests (validateTlsClientAuthClaimConfig_acceptsValid
NativeClaimMappings / _acceptsValidJsonStringClaimMappings) used a
pattern on subject_cn -- exactly the now-rejected shape -- and were
updated to use subject_ou instead, which still exercises the pattern
path meaningfully.
@fhanik
fhanik force-pushed the review/pr3792-fix branch 2 times, most recently from f52ba97 to dfc6578 Compare September 25, 2026 16:31
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 29, 2026
…other machine

Claude Code sessions are local JSONL transcripts keyed to the absolute project
path, so this session cannot be resumed on a different computer. This document
is the substitute: enough state, topology and hard-won environment detail to
pick the work up cold.

Records, verified against the repo rather than recalled:

- branch/PR state, the remotes to re-add (including rkoster's fork, since
  refs/pull/3972/head on the cloudfoundry remote is stale and will not update),
  and a commit-by-commit map of the branch
- the load-bearing fact that cloudfoundry#4076 implements RFC 8705 section 2.1.2 subject
  binding and cloudfoundry#3972 does not, so on that branch any certificate chaining to the
  configured CA authenticates as the client
- that the two branches have diverged (his 14 follow-ups start exactly where
  our squash ends) and cannot simply be merged
- verification commands and the current green state, including that the two
  remaining integrationTest failures are both SCIM rate-limiter artifacts
- environment traps that cost real time: piped gradle output hiding exit codes,
  ChromeDriver/Chrome major-version coupling and the unmanaged Homebrew binary,
  gradle daemons pinning a stale PATH, the Jackson 3 TypeReference package, and
  a local process that rewrites commits and strips trailers
- zone-aware MockMvc conventions, especially why servletPath may only be
  pre-set when no /z/{subdomain} prefix is present
- open items here and across all three approaches, plus a document index

Also corrects a stale assumption while writing it: origin/review/pr3968 is
twelve commits ahead of what was local, and includes two red/green pairs adding
JWT-SVID audit events and default-zone-only signing. Those close two findings
that pr3968-security-review.md still lists as unaddressed, so the handoff says
to take the remote and treat the old local branch as superseded.
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 29, 2026
Removes the JWT-SVID/SPIFFE material, which belongs to a separate branch that
will not be picked up on the new machine and was only noise for continuing
PR cloudfoundry#4076:

- drops the section on that branch's divergence from origin entirely
- section 10 becomes a two-way comparison of this branch against the Service
  Accounts RFC, which stays because that design depends on
  tls_client_auth_san_dns and therefore consumes this branch rather than
  competing with it
- the document index lists only the mTLS-relevant analysis docs
- the "verify, do not assert" agreement now cites mTLS examples: the seven
  stale FINDING labels, the dotted-claim protection we already had, and the CA
  rotation gap we actually did not
- the @ConditionalOnProperty note now describes how the mTLS suites switch the
  feature on, rather than pointing at the other branch's fixtures
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
…other machine

Claude Code sessions are local JSONL transcripts keyed to the absolute project
path, so this session cannot be resumed on a different computer. This document
is the substitute: enough state, topology and hard-won environment detail to
pick the work up cold.

Records, verified against the repo rather than recalled:

- branch/PR state, the remotes to re-add (including rkoster's fork, since
  refs/pull/3972/head on the cloudfoundry remote is stale and will not update),
  and a commit-by-commit map of the branch
- the load-bearing fact that cloudfoundry#4076 implements RFC 8705 section 2.1.2 subject
  binding and cloudfoundry#3972 does not, so on that branch any certificate chaining to the
  configured CA authenticates as the client
- that the two branches have diverged (his 14 follow-ups start exactly where
  our squash ends) and cannot simply be merged
- verification commands and the current green state, including that the two
  remaining integrationTest failures are both SCIM rate-limiter artifacts
- environment traps that cost real time: piped gradle output hiding exit codes,
  ChromeDriver/Chrome major-version coupling and the unmanaged Homebrew binary,
  gradle daemons pinning a stale PATH, the Jackson 3 TypeReference package, and
  a local process that rewrites commits and strips trailers
- zone-aware MockMvc conventions, especially why servletPath may only be
  pre-set when no /z/{subdomain} prefix is present
- open items here and across all three approaches, plus a document index

Also corrects a stale assumption while writing it: origin/review/pr3968 is
twelve commits ahead of what was local, and includes two red/green pairs adding
JWT-SVID audit events and default-zone-only signing. Those close two findings
that pr3968-security-review.md still lists as unaddressed, so the handoff says
to take the remote and treat the old local branch as superseded.
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
Removes the JWT-SVID/SPIFFE material, which belongs to a separate branch that
will not be picked up on the new machine and was only noise for continuing
PR cloudfoundry#4076:

- drops the section on that branch's divergence from origin entirely
- section 10 becomes a two-way comparison of this branch against the Service
  Accounts RFC, which stays because that design depends on
  tls_client_auth_san_dns and therefore consumes this branch rather than
  competing with it
- the document index lists only the mTLS-relevant analysis docs
- the "verify, do not assert" agreement now cites mTLS examples: the seven
  stale FINDING labels, the dotted-claim protection we already had, and the CA
  rotation gap we actually did not
- the @ConditionalOnProperty note now describes how the mTLS suites switch the
  feature on, rather than pointing at the other branch's fixtures
fhanik and others added 24 commits October 15, 2026 14:38
Require tls-client-auth-trusted-proxy-subject-dn alongside inline or named
proxy CA trust. Validate the genuine TLS peer chain, leaf constraints and
registered DN before trusting its forwarded workload certificate.

Validate the identity during client registration and bootstrap, preserve it
through typed/flat configuration and token enrichment, and deny legacy or
malformed proxy policy without falling back to direct authentication.
Document the required setting and update the client-admin API reference.

Validation: 576 model, 316 server and 137 HTTP tests passed; 11 model tests
skipped. Markdown lint and git diff --check pass. API docs regenerated
successfully with generateDocs.

ai-assisted=yes
Startup must stay fast on installations with tens of thousands of zones,
so the trust-bundle validator may only inspect the default zone at
startup, and only when mTLS is effectively enabled there (deployment
switch and zone switch). A disabled default zone or a disabled deployment
leaves its persisted references inert instead of failing startup.

Tenant zones are validated when an update turns mTLS on for that zone:
a client reference that does not resolve against the new configuration
rejects the update, a reference restored by the same update is accepted,
and updates that keep mTLS enabled (bundle removal) or disable it are not
revalidated. Covered both with the default zone disabled and enabled,
through PUT /identity-zones/{id} and at the unit level.

Require the ClientCertificateMapper not to be reflectively loaded when
uaa.mtls.enabled=false; the registration bean still exists, disabled,
around an inert pass-through delegate.

Add the reverse MockMvc zone policy case: disabling a tenant zone leaves
the default zone's mTLS endpoint working (passes already).

The validator gains the deployment flag and a no-op validateOnEnable so
the tests compile. Eleven cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…een)

Startup validates only the default zone, and only when mTLS is enabled
for both the deployment and that zone, so startup time no longer grows
with the number of identity zones and a disabled feature leaves stale
references inert instead of failing startup.

PUT /identity-zones/{id} validates a zone's client bundle references
when the update turns mTLS on, answering 422 when a reference does not
resolve against the new configuration. Updates that keep mTLS on or
turn it off are not revalidated; token-time resolution still denies.
Both directions are independent of the default zone's own policy.

The ClientCertificateMapper is no longer reflectively loaded when
uaa.mtls.enabled=false; its disabled registration wraps a pass-through.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validating trust-bundle references loaded every client of the zone,
although almost none of them carry an mTLS configuration. Specify a
zone-scoped client query that filters on the additional information key
in the database: it returns only clients whose additional information
has one of the given keys, ignores clients in other zones and values
that merely contain the key text, treats LIKE wildcards in keys
literally, and returns nothing without keys.

Require the trust-bundle validator to use that query at startup and on
zone enable, and never call listClientDetails.

A scaffold of the query that still lists every client lets the tests
compile. Nine cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MultitenantJdbcClientDetailsService filters a zone's clients in SQL on
the serialized additional information key, with LIKE wildcards escaped,
and confirms the match on the parsed map. The base class keeps an
in-memory fallback for non-JDBC implementations.

The trust-bundle validator uses this query at startup and on zone
enable, so it no longer loads every client of a zone when almost none
of them have an mTLS configuration.

Also fix the new no-full-scan test, whose tenant stub returned an
unresolvable reference and made the enable check throw as designed.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Even a filtered query has to read every client row of a zone, because
bundle references live in the additional_information JSON column.
Token-time resolution already denies a client whose reference does not
resolve, so the early check is not worth a zone-wide scan.

Startup may check only the default zone's own bundle definitions and
must not touch client details. Enabling mTLS for a zone is accepted even
when a persisted client reference does not resolve; that client is
denied at token time and works again once the bundle is restored. Both
directions of the default zone policy are covered.

Three cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Startup checks only the default zone's own bundle definitions. Zone
updates no longer look up the zone's clients when mTLS is turned on, and
the LIKE query over additional_information is removed together with its
tests: it could not use an index and still read every client row of the
zone. A client reference that does not resolve is denied at token time.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Require config.mtls.enabled to default to false for new zones and for
stored zone configuration that omits the switch, and the default zone to
stay disabled unless the deployment enables it at bootstrap. A new zone
does not expose the mTLS endpoint or advertise it in discovery until it
opts in, and discovery for a zone that has not opted in omits mTLS even
when the deployment enables it.

Tests that exercise mTLS behaviour now enable it explicitly: a JUnit
extension installs an mTLS-enabled default zone, and MockMvc zone
fixtures opt their zones in. These fixture changes pass against the
current default.

Also replace the startup-validator MockMvc test left over from the
client-scan removal: a removed bundle no longer prevents startup, and
removedBundleFailsClosedAtTokenTime covers the token-time denial.

Nine cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New zones, and stored zone configuration that omits the switch, now have
mTLS disabled. The default zone still follows uaa.mtls.enabled on every
restart, and its bootstrap default is false as well. Turning on the
deployment flag therefore enables mTLS only for the default zone; each
tenant zone opts in through the identity-zone API.

Update the configuration reference, client authentication and getting
started guides, and the identity-zone API field descriptions, which also
still claimed that removed bundle references are rejected at startup.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Excluding granted_scopes from the claims copied onto a refreshed access
token is a pre-existing bug for any deployment that registers a token
enhancer, independent of mTLS. It now lives on a separate branch based
on cloudfoundry/develop, with its own test.

This pull request must merge after that one: the mTLS enhancer is always
registered, so with mTLS enabled the copy loop runs and would otherwise
leak granted_scopes onto narrowed refreshed access tokens.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e (red)

Client create, update and uaa.yml bootstrap currently refuse any mTLS
configuration while mTLS is off for the deployment or the zone, and
never check whether it is well formed. Validation happens when the
client is written, so it costs nothing to apply it every time.

Require valid configuration (CA plus exactly one subject binding) to be
accepted, and malformed configuration (missing subject binding,
unparseable trust, bound tokens without trust) to be rejected, with the
deployment off, or with the deployment on and the zone off. Covered in
the three validators and through POST /oauth/clients.

Seventeen cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (green)

Client create, update and uaa.yml bootstrap validate mTLS configuration
the same way regardless of the deployment and zone switches: valid
configuration is stored, malformed configuration is rejected. Whether it
is used is still decided per request, so nothing is ever rechecked by
scanning stored clients.

The deployment flag was only consulted for that refusal, so
ClientAdminEndpointsValidator, ClientAdminBootstrap and
ZoneEndpointsClientDetailsValidator get their develop constructor
signatures back.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (red)

UaaAuthenticationDetails is Serializable, is held by the security
context that Spring Session stores, and has no explicit
serialVersionUID. The new servletPath field and getter change the
computed UID, so during a rolling upgrade a node cannot read a session
written by a node on the other version.

Pin the previous release's computed UID, and deserialize an instance
serialized by cloudfoundry/develop (623f6ea). Both fail: the second
with InvalidClassException.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(green)

Pin UaaAuthenticationDetails' serialVersionUID to the value computed for
cloudfoundry/develop, so sessions round-trip between nodes on either
version during a rolling upgrade. A previous-release instance reads back
with a null servletPath, which the mTLS path check treats as not mTLS.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(red)

ClientAuthentication is public API of the model jar and backs external
IdP validation. Adding tls_client_auth to UAA_SUPPORTED_METHODS and
rewriting the three-argument isValidMethod changed results that have
nothing to do with mTLS: private_key_jwt without a key and none with a
key stopped validating, and tls_client_auth became a supported method.

Pin the previous release's results. Fails at the intended assertion.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…green)

Restore ClientAuthentication to its cloudfoundry/develop form apart from
the new TLS_CLIENT_AUTH constant. tls_client_auth is accepted only by
UAA's own mTLS token endpoint and is not one of UAA_SUPPORTED_METHODS,
which governs how UAA authenticates to external identity providers.

The external-IdP-only method list and the four-argument isValidMethod
and getCalculatedMethod are no longer needed and are removed, along with
their tests and the guards in ExternalOAuthAuthenticationManager that
only existed because tls_client_auth had become a supported method. The
external IdP validator and factory bean are identical to develop again;
their tests that reject tls_client_auth still hold.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mTLS off, /.well-known/openid-configuration now always carries
"tls_client_certificate_bound_access_tokens": false, a field the
previous release never emitted. Require it to be omitted unless mTLS is
enabled for the deployment and the zone, which RFC 8705 section 3.3
defines as false, and document the field as optional.

Five cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Omit tls_client_certificate_bound_access_tokens from the discovery
document unless it is true. The serialized discovery fixture is back to
its cloudfoundry/develop form.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every zone's config now serializes an "mtls" block in API responses and
in the stored configuration, and a PUT with "mtls": null or
"enabled": null is refused with 400, where the previous release ignored
the key. Rejecting null made sense while the zone default was true (a
null would have opted in); with the default now false a null means the
disabled default.

Require the block to be omitted while it holds its default, and nulls to
read as the disabled default, through JSON and PUT /identity-zones/{id}.

Five cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Omit config.mtls from serialized zone configuration while it equals the
default, and read "mtls": null and "enabled": null as the disabled
default instead of refusing them.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… test

ClientAdminBootstrap regained its develop constructor when client
validation stopped depending on the deployment flag; the integration
test source set still passed the removed argument.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove statements left over from the earlier rules: registration no
longer depends on uaa.mtls.enabled, null zone mtls values read as the
disabled default, and malformed default-zone bundles stop startup only
while mTLS is enabled there.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng (red)

A client with tls-client-auth-trusted-proxy-ca(-ref) and no workload
trust can be registered, can never authenticate at /oauth/mtls/token,
and, once stored, still authenticates with its secret at /oauth/token,
contrary to proxy trust marking a client as mTLS-only.

Require registration (client admin API and uaa.yml bootstrap) to reject
proxy trust without tls-client-auth-ca(-ref), and a stored proxy-only
client, including one with only the proxy subject DN, to be refused at
both token endpoints. A client with both kinds of trust stays valid.

Also require clearing a client's typed mTLS configuration to remove
tls_client_certificate_bound_access_tokens, which otherwise leaves a
binding policy that no token can satisfy.

Seven cases fail at the intended assertions.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g (green)

Registration rejects tls-client-auth-trusted-proxy-ca(-ref) without
tls-client-auth-ca(-ref), after the existing checks so malformed values
keep their specific errors. ClientDetailsAuthenticationProvider treats
any proxy trust key (TlsClientAuthConfiguration.PROXY_TRUST_KEYS) as
mTLS-only and refuses a stored client that has it without workload
trust, instead of letting it authenticate with its secret.
UaaClientDetails removes tls_client_certificate_bound_access_tokens when
the typed mTLS configuration is cleared.

Document the requirement for the proxy trust properties.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fhanik

fhanik commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

PR #4076 review — open findings and next steps (2026-10-15)

Pull request: #4076 (RFC 8705 tls_client_auth, supersedes #3972).
Working branch: review/pr3792-fix in this repository.

Where things stand

Item State
Local review/pr3792-fix 4abe4fe20, rebased onto cloudfoundry/develop 2d7acc4a5 (which includes #4107). Not pushed.
origin/review/pr3792-fix (PR head) bec01f667, the pre-rebase head
Backup of the pre-rebase head local branch backup/pr3792-fix-before-rebase (bec01f667)
Rebase check the rebased tree equals the backup tree plus exactly #4107's diff (UaaTokenServices and UaaTokenServicesTests)
Full suite on the rebased branch ./gradlew --offline clean test integrationTest: all pass except two ScimGroupEndpointsIntegrationTests cases that hit the SCIM rate limiter (429), which is expected locally
Review threads on #4076 all resolved (24)
#4107 (granted_scopes refresh fix) merged into develop

To publish the rebase: git push --force-with-lease=review/pr3792-fix:bec01f667 origin review/pr3792-fix.

Ground rules agreed with the maintainer

  • With uaa.mtls.enabled=false and no zone configuration there must be no performance cost and no behaviour
    change
    , and nothing may break the first startup after rollout.
  • No loops over all zones or all clients, at startup or at request time. Installations have tens of thousands of
    zones, and almost no clients use mTLS. Startup checks only the default zone.
  • Validate client configuration when it is written (create, update, uaa.yml bootstrap), whether or not mTLS
    is enabled. Don't go looking for broken stored clients later. At runtime, malformed stored values fail closed.
  • config.mtls.enabled defaults to false: tenant zones opt in. The default zone follows uaa.mtls.enabled on
    every restart.
  • Work test-first: a red commit with failing tests (compile-only scaffolding allowed), then a green commit with the
    production code. Commit messages end with ai-assisted=yes and the co-author line.
  • Run ./gradlew clean test integrationTest before reporting done. Use --offline while the Broadcom Artifactory
    mirrors return 403. SCIM 429 failures in integration tests are expected locally.
  • Don't introduce new issues. Keep fixes minimal, and avoid refactors mixed with behaviour changes.

Findings from the Copilot-style review of the rebased branch

The /code-review high run reported ten findings. Findings 1–4 and 7 were checked directly against the code. The
others were reviewed but not reproduced. Nothing below has been fixed yet.

Fix before merge

1. Enabling mTLS in a zone changes refresh-token access tokens for every client there

  • Where: server/.../oauth/UaaTokenServices.java, getAdditionalRootClaims. The gate is
    uaaTokenEnhancers.stream().anyMatch(UaaTokenEnhancer::isEnabled).
  • What: MtlsClaimsEnhancer.isEnabled() is true wherever mTLS is effective, so the refresh path copies the refresh
    token's additional root claims onto the new access token for every client in that zone. With granted_scopes
    now filtered (granted scopes refresh token #4107), what still gets copied is amr, acr, az_attr and any external attributes stored on the
    refresh token. On develop the copy only runs when a deployment registers its own enhancer.
  • Why it can be excluded: /oauth/mtls/token issues only client_credentials and never returns a refresh token,
    so the mTLS enhancer contributes nothing on refresh.
  • Recommendation: do not let the mTLS enhancer enable the refresh copy (keep develop's behaviour for mTLS zones).
    Add a test that refreshes an ordinary client in an mTLS-enabled zone and asserts the access token matches the
    mTLS-off result.

3. Bootstrap clients are validated against a stale copy of the default zone

  • Where: server/.../client/ClientAdminBootstrap.java, about line 234:
    identityZoneManager.getCurrentIdentityZone().
  • What: IdentityZoneHolder caches the zone per thread (ThreadLocal.withInitial).
    IdentityZoneConfigurationBootstrap saves the new config (including uaa.mtls.trust_bundles) through
    provisioning but never refreshes that cache. @DependsOn("identityZoneConfigurationBootstrap") only orders bean
    creation.
  • Failure: a deploy that adds a bundle and an oauth.clients entry with tls-client-auth-ca-ref naming it, in the
    same change, can fail first startup with "unknown or empty mTLS trust bundle". This is plausible but not
    reproduced, and no full-context test covers bootstrapping a client that uses a -ca-ref.
  • Recommendation: validate bootstrap clients against the default zone read fresh from provisioning (one read at
    startup). Add a full-context test with a bundle in uaa.mtls.trust_bundles and a bootstrap client referencing it.

2. The docs about startup bundle validation are wrong (my error earlier the same day)

  • Where: docs/UAA-Client-Authentication.md (bundle section, around "Startup checks only the default zone's own
    bundle definitions…") and docs/UAA-Configuration-Reference.md (uaa.mtls.trust_bundles: "while mTLS is enabled for
    the default zone, prevent startup").
  • What: GeneralIdentityZoneConfigurationValidator validates config.mtls.trustBundles on every CREATE/MODIFY,
    and IdentityZoneConfigurationBootstrap runs MODIFY on every startup. So malformed uaa.mtls.trust_bundles stop
    startup even with mTLS off. This is write-time validation of operator configuration, so the behaviour is fine;
    the docs are wrong. The bundle check in MtlsTrustBundleStartupValidator is now redundant.
  • Recommendation: keep the behaviour. Restore the docs to "malformed definitions prevent startup". Remove the
    redundant check from MtlsTrustBundleStartupValidator, or reduce that class to nothing and delete it.
    MtlsTrustBundleStartupValidatorTest.deploymentSwitchOffSkipsValidation passes only because it tests that class
    alone, not the real startup path.

One rule to end the review cycle (findings 4, 6, 7)

4. mTLS clients can be registered with grant types that can never work

  • Where: server/.../client/ClientAdminEndpointsValidator.java (validateMtlsClientConfiguration has no
    grant-type rule).
  • What: authorization_code, refresh_token, password, jwt-bearer and token-exchange are accepted for a
    client with workload trust, and none of them can be redeemed. With implicit and bound tokens off,
    /oauth/authorize issues tokens for a "certificate-only" client with no certificate.

6. mTLS-only settings are accepted without workload trust and then silently ignored

  • What: tls-client-auth-required-claims, claim mappings, sub/aud templates, allowed-resources and the five
    subject-binding parameters are accepted without tls-client-auth-ca(-ref). At runtime the client is an ordinary
    secret client. An operator may believe the client is pinned to an org, space or app when it is not. Proxy trust
    without workload trust is already rejected (bec01f667); these keys need the same rule.

7. Malformed stored values return HTTP 500 instead of 400/401

  • Where: server/.../oauth/tls/MtlsClaimsEnhancer.java (isBoundAccessTokens and the
    "configuration cannot be resolved" path throw IllegalStateException) and ClientDetailsAuthenticationProvider.
    The provider only checks the bound flag when no CA is configured
    (!tlsClientAuthConfigured && !Boolean.FALSE.equals(boundAccessTokens)).
  • Failure: a stored client with a CA, a subject binding and tls_client_certificate_bound_access_tokens: "yes",
    or malformed allowed-resources, authenticates and then gets a 500 at issuance. This only affects stored data that
    bypassed registration.

Recommendation for 4, 6 and 7: one shared definition in TlsClientAuthConfiguration (the set of all mTLS keys and
the workload trust keys), enforced in two places:

  • Registration: any mTLS key requires workload trust, and a client with workload trust may only have
    client_credentials.
  • Runtime (ClientDetailsAuthenticationProvider): any mTLS key makes a client mTLS-only. An unparseable bound
    flag, or configuration the enhancer cannot read, fails authentication with 401, not issuance with 500.

The runtime cost on non-mTLS requests is a few map lookups at client authentication. One red/green pair; keep the
existing per-key error messages by running the new rules after the existing well-formedness checks, as was done for
proxy trust.

Docs only

5. mTLS clients also lose Basic auth at /check_token and /introspect

  • This follows from "every other credential path is refused"
    (ClientDetailsAuthenticationProvider: configured clients outside /oauth/mtls/token are refused). The docs only
    name /oauth/token. Name the other endpoints in docs/UAA-Client-Authentication.md and the slate source. No code
    change.

10. Matching rules for subject values are undocumented

  • TlsClientAuthSubjectMatcher compares DN attribute values case-insensitively (javax.naming.ldap.Rdn equality),
    which follows RFC 8705's DN matching rules. dNSName matching ignores case; rfc822Name and URI matching are exact.
    Document this and add tests that fix the current behaviour.

Defer (not correctness; mTLS requests only)

  • 8: each proxied mTLS token request validates the proxy chain twice (provider, then MtlsClaimsEnhancer), and the
    client row is loaded up to four times (provider, UaaTokenEndpoint.allowedResourcesFor, the enhancer,
    UaaTokenServices). Option: keep the validated chain and parsed config as request attributes.
  • 9: the code that turns a client's stored additionalInformation into a TlsClientAuthConfiguration is
    duplicated in ClientDetailsAuthenticationProvider.getTlsClientAuthConfiguration,
    MtlsClaimsEnhancer.loadTlsConfig, UaaTokenEndpoint.allowedResourcesFor and the validator. The copies already
    differ: the provider omits allowedResources, and the enhancer omits subject bindings and returns null where the
    provider throws. This is the root cause of the recurring Copilot findings. Replace them with one
    TlsClientAuthConfiguration.fromAdditionalInformation(Map) in the model module, as a separate, test-pinned refactor
    after the fixes above.

Proposed order when resuming

  1. Decide whether to push the rebased branch first (force-push with lease; see above).
  2. Finding 1: red/green.
  3. Finding 3: red/green, including the full-context bootstrap test.
  4. Finding 2: docs, plus removal of the redundant startup check (red/green if the validator changes).
  5. Findings 4, 6, 7: one red/green pair with the shared key definitions.
  6. Findings 5 and 10: docs, plus tests that fix current behaviour for 10.
  7. Full suite, push, reply to and resolve any new Copilot threads, update the PR description's "Compatibility notes"
    and "With mTLS off" sections if behaviour changed.
  8. Later, separate change: findings 8 and 9.

Useful references

  • PR description source used for the last update:
    /private/tmp/claude-501/-Users-fh012259-workspace-cloudfoundry-uaa/f23bc15e-0ccd-4c77-bce8-a92ba4f35347/scratchpad/pr-body-new.md
    (session scratchpad; it may be gone, in which case read the current body with
    gh api repos/cloudfoundry/uaa/pulls/4076 --jq .body).
  • Review threads: gh api graphql on pullRequest(number:4076) { reviewThreads { … } }. Reply with
    addPullRequestReviewThreadReply and resolve with resolveReviewThread. In zsh, arrays are 1-indexed.

fhanik and others added 3 commits October 15, 2026 15:36
Tests only; production code is unchanged. Each failing test pins one
finding from the review of the rebased branch, and guard tests pin the
behaviour that must not change, so the fix can be checked against both.

Finding 1, refreshed access tokens in mTLS zones
(UaaTokenServicesTests$WhenRefreshGrant):
  - anActiveMtlsEnhancerDoesNotChangeRefreshedAccessTokens fails: an
    enabled MtlsClaimsEnhancer makes a refreshed access token gain amr,
    acr and the refresh token's external attributes.
  - guard: aCustomEnhancerStillReceivesRefreshTokenClaims... passes;
    develop's copy for custom enhancers must stay.

Finding 3, bootstrap against a stale zone (ClientAdminBootstrapTests):
  - bootstrapClientBundleReferenceIsValidatedAgainstTheSavedDefaultZone
    fails with "unknown or empty mTLS trust bundle": the per-thread
    IdentityZoneHolder copy predates the saved uaa.mtls.trust_bundles.

Finding 4, grant types (ClientAdminEndpointsValidatorTests):
  - rejectsGrantTypesAnMtlsClientCanNeverUse (authorization_code,
    password, refresh_token) and rejectsImplicitForAnMtlsClient fail:
    a client with workload trust may only use client_credentials.
  - guards: the same grants stay valid without mTLS.

Finding 6, mTLS-only settings without workload trust:
  - registration (ClientAdminEndpointsValidatorTests): subject bindings,
    required claims, claim mappings, sub/aud templates and allowed
    resources without tls-client-auth-ca(-ref) are accepted today.
  - runtime (MtlsTokenEndpointHardeningMockMvcTests$
    StoredConfigurationFailsClosed): a stored client with them gets a
    token with its secret (200) instead of 401 invalid_client.

Finding 7, stored values that cannot be read
(StoredConfigurationFailsClosed):
  - an unreadable tls_client_certificate_bound_access_tokens or
    tls-client-auth-allowed-resources returns 500 server_error instead
    of 401 invalid_client.

Constraints for the production change: run new rules after the
existing well-formedness checks so existing error messages are kept;
keep the existing message for bound tokens without trust; no lookups on
non-mTLS requests beyond the client's own additional information.

Findings 2, 5 and 10 are documentation changes and have no failing
test. Twenty-one cases fail, all new; every other test passes.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The specification commit requires an mTLS client to use only
client_credentials, the only grant /oauth/mtls/token issues. Six
bootstrap tests built their mTLS client from createClientMap, whose
default grant is authorization_code, so they contradict that rule.
They test bootstrapping mTLS settings, not grant types; give their
clients client_credentials.

This includes the red stale-zone test from the specification commit,
which would otherwise fail on the grant rule instead of the zone it
pins. Without production changes the five existing tests still pass,
and the stale-zone test still fails for its original reason.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Makes the tests in 9f4daf8 pass without changing them; every
existing message and behaviour outside mTLS clients is kept.

Finding 1: MtlsClaimsEnhancer no longer turns on the refresh-claim copy
in UaaTokenServices. It only contributes at /oauth/mtls/token, which
never returns a refresh token, so other clients' refreshed access
tokens in an mTLS zone stay as they are without it. Custom enhancers
keep develop's copy.

Finding 3: ClientAdminBootstrap validates uaa.yml clients against the
default zone read once from identityZoneProvisioning, as the zone
bootstrap saved it, instead of a per-thread IdentityZoneHolder copy
that may predate uaa.mtls.trust_bundles. It reads only when there are
clients to bootstrap, and falls back to the holder without the bean.

Findings 4 and 6, registration: validateMtlsClientRules, called after
the existing checks on the client admin API, zone client API and
bootstrap, rejects certificate-only settings (subject binding, claim
mappings, templates, required claims, allowed resources) without
tls-client-auth-ca(-ref), and any grant other than client_credentials
for a client with workload trust. A null value counts as absent.

Findings 6 and 7, runtime: ClientDetailsAuthenticationProvider refuses
a stored client with certificate-only settings but no workload trust,
and an mTLS client whose tls_client_certificate_bound_access_tokens or
tls-client-auth-allowed-resources cannot be read, with 401
invalid_client naming the setting instead of a secret fallback or a
500 at issuance. Non-mTLS clients pay only a few map lookups.

Docs: describe the new rules and correct the startup note; malformed
uaa.mtls.trust_bundles prevent startup whether or not mTLS is enabled.

Model, server and uaa unit and MockMvc tests pass, apart from a
timing-sensitive RefreshRotationTest that passes on re-run. Two full
clean test integrationTest runs were cut off by system sleep, so the
integration tests have not yet run with this change.

ai-assisted=yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fhanik
fhanik force-pushed the review/pr3792-fix branch from bec01f6 to 5bcaa49 Compare October 16, 2026 11:41
@fhanik

fhanik commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #4109, which carries the same code (0-line diff against this branch's final state) rebased onto develop after #4107, in three commits: rkoster's original commit unchanged, then the tests with the plumbing they need to run and fail, then the production code.

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

Labels

Projects

Development

Successfully merging this pull request may close these issues.

4 participants