Skip to content

feat: RFC 8705 mutual-TLS client authentication for CF app instance identity - #3972

Draft
rkoster wants to merge 98 commits into
cloudfoundry:developfrom
rkoster:feat/rfc8705-mtls-client-auth
Draft

feat: RFC 8705 mutual-TLS client authentication for CF app instance identity#3972
rkoster wants to merge 98 commits into
cloudfoundry:developfrom
rkoster:feat/rfc8705-mtls-client-auth

Conversation

@rkoster

@rkoster rkoster commented Jul 3, 2026

Copy link
Copy Markdown

Summary

Implements RFC 8705 mutual-TLS client
authentication for Cloud Foundry app instance identity, enabling workload identity
federation with AWS, GCP, Azure, and any OIDC-aware service.

CF app instances already receive a short-lived X.509 certificate from the Diego
Cell (instance.crt / instance.key). This change lets an app exchange that cert
for a UAA JWT containing verified app_guid, space_guid, org_guid, and
cf_instance_guid claims — without secrets or user credentials.

How it works

CF app ──cert──▶ Gorouter (sanitize_set) ──XFCC──▶ UAA /oauth/mtls/token
                                                         │
                                            ClientCertificateMapper
                                            (XFCC → X509Certificate attr)
                                                         │
                                            ClientDetailsAuthenticationProvider
                                            (tls_client_auth: PKIX chain validation)
                                                         │
                                            MtlsClaimsEnhancer
                                            (cert OU → app/space/org_guid claims)
                                                         │
                                            ◀── JWT with CF identity claims ──

Changes (this PR — 18 commits)

Model layer:

  • ClientAuthentication: add tls_client_auth constant
  • TokenConstants: add CLIENT_AUTH_TLS_CLIENT_AUTH
  • TlsClientAuthConfiguration: per-client CA PEM + claim-mapping model
  • UaaClientDetails: add tlsClientAuthConfiguration field
  • OpenIdConfiguration: add mtls_endpoint_aliases to OIDC discovery

Server layer:

  • ClientDetailsAuthenticationProvider: isTlsClientAuth(), validateTlsClientAuth(), getTlsClientAuthConfiguration() — handles in-memory, Map (Jackson), and flat String PEM (BOSH) config forms
  • TlsClientAuthentication: PKIX cert chain validation against per-client CA
  • ClientCertificateMapper registration: SpringServletXmlFiltersConfiguration registers the java-buildpack-client-certificate-mapper-jakarta filter for /oauth/mtls/* to materialise X-Forwarded-Client-Cert as a jakarta.servlet.request.X509Certificate attribute
  • ClientCredentialsTokenGranter: allow tls_client_auth alongside client_secret
  • MtlsClaimsEnhancer: UaaTokenEnhancer that reads cert subject OU fields and maps them to JWT claims per per-client configuration; handles DB-loaded clients (reads additionalInformation directly) and Diego multi-valued RDNs
  • FilterChainOrder.OAUTH_11 + mtlsTokenEndpointSecurity: dedicated security filter chain for /oauth/mtls/token with CSRF disabled
  • UaaTokenEndpoint: add /oauth/mtls/token to @RequestMapping
  • OIDC discovery: expose mtls_endpoint_aliases

Deployment notes

Requires the Gorouter to be configured with forwarded_client_cert: sanitize_set
so it validates the TLS session cert and injects it as X-Forwarded-Client-Cert.

The UAA client for an app must be configured with:

token-endpoint-auth-method: tls_client_auth
tls-client-auth-ca: <instance-identity CA certificate PEM>
tls-client-auth-trusted-proxy-ca: <Gorouter backend mTLS CA certificate PEM, e.g. service_cf_internal_ca>
tls-client-auth-claim-mappings:
  - field: subject_cn
    claim: cf_instance_guid
  - field: subject_ou
    pattern: "app:(.+)"
    claim: app_guid
  - field: subject_ou
    pattern: "space:(.+)"
    claim: space_guid
  - field: subject_ou
    pattern: "organization:(.+)"
    claim: org_guid

tls-client-auth-trusted-proxy-ca switches this client to the Gorouter/XFCC-forwarding-only
topology: UAA then requires the X-Forwarded-Client-Cert header to actually be present and its
immediate TLS peer to have presented a certificate signed by that CA during the handshake --
preventing a direct caller (bypassing the Gorouter) from replaying a harvested certificate it
doesn't hold the private key for, or a direct connection from being silently accepted instead.
For a client that connects to UAA directly (e.g. permitted by Application Security Group
configuration, bypassing the Gorouter), omit this property entirely -- configuring it at all
makes the client reject direct connections. See
docs/UAA-Client-Authentication.md
for both cases; two separate UAA clients are needed to support both patterns for the same
workload.

Proof of concept

End-to-end verified on a real CF deployment: a Go app pushes its Diego instance cert
to POST /oauth/mtls/token, and the returned JWT contains:

{
  "app_guid":         "b0bff1c2-a258-4060-981d-601f22e6bcf8",
  "space_guid":       "02700fa7-8db7-4598-b015-9a5fc73d4656",
  "org_guid":         "8deb6c47-8460-4501-8a86-246b774d97e4",
  "cf_instance_guid": "86bf36e4-af79-4d7a-6484-0d89",
  "client_auth_method": "tls_client_auth",
  "cnf": { "x5t#S256": "rk4P4d0DXNJDpZeOotKRUzmoaomqSqPQn8OzyKQhMuw" }
}

All GUIDs verified against cf app, cf org, and cf space --guid.

Related

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.

Pull request overview

This PR adds RFC 8705 mutual-TLS client authentication support for Cloud Foundry app instance identity by introducing a dedicated /oauth/mtls/token endpoint, validating instance certificates against per-client CA configuration, and enriching issued JWTs with CF identity claims derived from certificate subject fields. It also updates OIDC discovery to advertise mTLS endpoint aliases and tls_client_auth as a supported token endpoint authentication method.

Changes:

  • Introduces /oauth/mtls/token with a dedicated Spring Security filter chain and request-to-certificate mapping via ClientCertificateMapper.
  • Adds TLS client certificate validation (TlsClientAuthentication) and a token enhancer (MtlsClaimsEnhancer) to emit cnf.x5t#S256 plus configured subject-derived claims.
  • Extends client auth method support across model/constants and OIDC discovery metadata (tls_client_auth, mtls_endpoint_aliases).

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthenticationTest.java Adds unit coverage for null inputs and malformed CA handling in TLS cert validation.
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/tls/MtlsClaimsEnhancerTest.java Verifies OU/CN claim extraction and cnf.x5t#S256 behavior for mTLS tokens.
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/tls/ClientCertificateMapperFilterTest.java Confirms servlet filter registration for mapping XFCC to request X509Certificate attribute on /oauth/mtls/*.
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/provider/client/ClientCredentialsTokenGranterTests.java Ensures tls_client_auth is allowed for client_credentials.
server/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaClientAuthenticationProviderTest.java Updates provider wiring to include TlsClientAuthentication.
server/src/test/java/org/cloudfoundry/identity/uaa/authentication/ClientDetailsAuthenticationProviderTests.java Adds tests for mtls path detection and TLS config deserialization behavior.
server/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpointsTest.java Validates discovery document includes mtls_endpoint_aliases.token_endpoint.
server/src/main/java/org/cloudfoundry/identity/uaa/web/FilterChainOrder.java Adds a new security chain order slot (OAUTH_11) for the mTLS token endpoint chain.
server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlFiltersConfiguration.java Registers ClientCertificateMapper filter for /oauth/mtls/*.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenEndpoint.java Expands token endpoint mapping to include /oauth/mtls/token.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthentication.java Adds per-client CA-based PKIX validation and request certificate extraction helper.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/MtlsClaimsEnhancer.java Implements JWT enrichment from cert subject + cnf.x5t#S256 for the mTLS flow.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/provider/client/ClientCredentialsTokenGranter.java Allows tls_client_auth as a valid auth method for client credentials.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointSecurityConfiguration.java Adds dedicated security filter chain for /oauth/mtls/token (stateless + CSRF disabled).
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java Wires TlsClientAuthentication into ClientDetailsAuthenticationProvider bean construction.
server/src/main/java/org/cloudfoundry/identity/uaa/authentication/ClientDetailsAuthenticationProvider.java Detects mTLS path, validates certs, and parses per-client TLS configuration from additional info.
server/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpoints.java Populates mtls_endpoint_aliases in OIDC discovery.
server/build.gradle.kts Adds dependency on the Gorouter client certificate mapper (Jakarta).
model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.json Updates fixture to include tls_client_auth in supported auth methods.
model/src/test/java/org/cloudfoundry/identity/uaa/constants/ClientAuthenticationTest.java Adds tests for tls_client_auth support, secret requirements, and validity rules.
model/src/test/java/org/cloudfoundry/identity/uaa/client/UaaClientDetailsTest.java Adds JSON round-trip test for TLS client auth config and adjusts hashCode assertion.
model/src/test/java/org/cloudfoundry/identity/uaa/client/TlsClientAuthConfigurationTest.java Adds unit tests for TLS auth config JSON round-tripping and equality semantics.
model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java Updates supported auth methods expectations and adds tests for mTLS aliases field.
model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/TokenConstants.java Exposes CLIENT_AUTH_TLS_CLIENT_AUTH constant.
model/src/main/java/org/cloudfoundry/identity/uaa/constants/ClientAuthentication.java Adds TLS_CLIENT_AUTH constant and updates supported/valid method logic and calculation.
model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java Introduces tlsClientAuthConfiguration field and includes it in equals/hashCode.
model/src/main/java/org/cloudfoundry/identity/uaa/client/TlsClientAuthConfiguration.java Adds model for trusted CA PEM + claim mapping configuration.
model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java Adds mtls_endpoint_aliases and includes tls_client_auth in supported methods.

Comment thread server/build.gradle.kts Outdated

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.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.

Comment thread model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java Outdated

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.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 4 comments.

rkoster added 15 commits August 18, 2026 08:50
Add TlsClientAuthConfiguration field serialized as tls-client-auth-ca in
client JSON, following the clientJwtConfig pattern. Includes getter/setter,
copy constructor support, equals/hashCode. Fix fragile isPositive() hash
code assertion to isNotZero().
…dpoint

The BOSH ERB template emits 'mtls.endpoint' (from the nested mtls.endpoint
YAML block) but the @value annotation was reading 'uaa.mtls_endpoint_path',
a key never emitted by the template. Align the annotation to the actual
Spring property so operator-configured paths are honoured.
The previous validation only checked that required-claims KEYS reference a
declared claim -- it never checked that VALUES are non-null. A null value
reaches TlsClientAuthentication.certificateSatisfiesRequiredClaims's
required.getValue().equals(...) on EVERY authentication attempt for that
client, throwing an unhandled NullPointerException per-request rather than
once at client creation/update time -- a worse blast radius than the original
bugs this validation exists to prevent.

Also strengthens the JSON-string-vs-native-object test to parse identical
logical claim-mapping data via both shapes, genuinely proving parsing
equivalence rather than exercising each shape with different data.

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.

Pull request overview

Copilot reviewed 59 out of 59 changed files in this pull request and generated 1 comment.

Suppressed comments (11)

server/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpoints.java:40

  • The mTLS alias is emitted even when uaa.mtls-enabled is false (the default), while that setting prevents mTLS client configuration and leaves Tomcat unable to request peer certificates. Discovery will therefore claim tls_client_auth support on deployments where it cannot work. Gate both this alias and token_endpoint_auth_methods_supported on the feature flag.
        String contextPath = getServerContextPath(request);
        OpenIdConfiguration conf = new OpenIdConfiguration(contextPath, getTokenEndpoint());
        conf.setMtlsEndpointAliases(Map.of("token_endpoint", contextPath + mtlsEndpointPath));

server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthentication.java:457

  • Rdn.toAttributes() represents repeated attributes in one multi-valued RDN (for example OU=app:x+OU=space:y) as one Attribute with multiple values, but attr.get() returns only one of them. Consequently extractOus loses the remaining OU values despite this helper claiming multi-valued-RDN support. Iterate attr.getAll() when collecting OUs.
                Attribute attr = attrs.next();
                if (attr.getID().equalsIgnoreCase(type)) {
                    Object value = attr.get();
                    return value == null ? null : value.toString();

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:444

  • Compilation alone is insufficient because matchFirstOu only emits group(1). A valid pattern with no capture group is accepted here but can never produce its configured claim, potentially making required-claim authentication fail permanently. Reject patterns whose matcher has fewer than one capture group.
                try {
                    Pattern.compile(pattern);

server/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpoints.java:23

  • mtls.endpoint only changes discovery metadata; the controller mapping and security matcher remain hard-coded to /oauth/mtls/token. Any override therefore advertises a non-existent endpoint. Remove this configurability or use the same setting for routing/security as well.

This issue also appears on line 38 of the same file.

    @Value("${mtls.endpoint:/oauth/mtls/token}")
    private String mtlsEndpointPath = "/oauth/mtls/token";

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java:223

  • Bootstrapped clients only run the feature-gate check, so malformed claim mappings/templates accepted here bypass the new validation used by the admin and zone APIs. For example, an invalid regex is persisted and later throws during token issuance. Invoke validateTlsClientAuthClaimConfig before persisting bootstrap clients too.
            client.setAdditionalInformation(info);
            ClientAdminEndpointsValidator.checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, clientId);

server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneEndpointsClientDetailsValidator.java:60

  • A mere key presence is treated as a usable TLS credential. A null/blank tls-client-auth-ca therefore bypasses the required-secret check, but later TlsClientAuthConfiguration.isConfigured rejects it, creating a secretless client that can never authenticate. Base this decision on a parsed, nonblank CA configuration.
    server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthentication.java:236
  • pattern is accepted for all three fields, but extraction applies it only to subject_ou; patterns configured for subject_cn or subject_o are silently ignored and the unfiltered value is emitted. Apply the pattern consistently to all supported subject fields (or reject it for fields where it is unsupported).

This issue also appears on line 454 of the same file.

            String value = switch (mapping.getField()) {
                case "subject_cn" -> cn;
                case "subject_ou" -> matchFirstOu(ous, mapping.getPattern());
                case "subject_o"  -> extractRdnValue(dn, "O");
                default -> null;

server/src/main/java/org/cloudfoundry/identity/uaa/web/tomcat/MtlsClientAuthTomcatCustomizer.java:109

  • If a provider named BCJSSE is already registered in non-FIPS mode, this method silently reuses it, despite the connector and repository requiring the FIPS provider. Validate the existing provider's type and isFipsMode() value and fail fast rather than serving TLS through a non-FIPS implementation.
    docs/UAA-Client-Authentication.md:121
  • The setup instructions omit the required deployment-wide uaa.mtls-enabled: true switch. With the documented steps as written, the default is false and the validators reject every client containing these mTLS properties. Document the switch here and add it to docs/UAA-Configuration-Reference.md as required for new configuration properties.
Per-client properties (set via the client-admin API, `oauth.clients` bootstrap, or the client
admin UI, alongside the client's other properties such as `authorized-grant-types`):

docs/UAA-Client-Authentication.md:125

  • token-endpoint-auth-method is documented as required, but no client model, validator, bootstrap path, or authentication provider consumes this property; repository search finds it only in this document. Following the example merely stores an unused additional-information entry. Remove it from the required configuration or implement and validate the registration metadata.
| `token-endpoint-auth-method: tls_client_auth` | yes | Selects mTLS client authentication for this client. |

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:399

  • Returning whenever the mappings key is absent skips validation of the other three properties. For example, tls-client-auth-required-claims without mappings is accepted even though none of its keys can ever be extracted, so every authentication for that client fails. Continue with an empty declared-claims set and validate any templates/required claims that are present.

This issue also appears on line 443 of the same file.

    public static void validateTlsClientAuthClaimConfig(Map<String, Object> additionalInfo, String clientId) {
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

…t ReDoS

Pattern.compile("\\{([^}]+)\\}") in both ClientAdminEndpointsValidator and
MtlsClaimsEnhancer backtracks character-by-character on unmatched braces (e.g.
many consecutive '{' with no closing '}'), degrading to roughly O(n^2) across
Matcher.find()'s repeated scan attempts -- a polynomial-time denial-of-service
vector on operator-controlled tls-client-auth-sub-template/aud-templates values
(CodeQL: js/polynomial-redos, flagged on the newly-added
ClientAdminEndpointsValidator occurrence).

[^}]+ -> [^}]++ (possessive quantifier) eliminates backtracking entirely,
making every match attempt provably linear-time, with no change in matching
result for any well-formed input.

Adds a timing-based test (ClientAdminEndpointsValidatorTests) proving the
fixed regex completes quickly on a pathological input of many unmatched '{'
characters.
…cert-encoding failure

enhance() previously caught any exception from cert.getEncoded()/SHA-256
digesting and silently omitted the cnf.x5t#S256 confirmation claim, allowing
token issuance to continue -- silently downgrading a certificate-bound (RFC
8705 sec:3.1 sender-constrained) mTLS token into an ordinary, unbound bearer
token on what should be a practically-impossible encoding failure. Now
rethrows as IllegalStateException, which (per the same unguarded
enhancer-loop precedent established for the client-lookup fail-open fix in
1f39eaf) propagates through enhance() and fails the whole token request
instead of silently issuing an unbound one.

Also fixes extractsClaimsFromCertOuFields, a pre-existing test that didn't
stub cert.getEncoded() and was incidentally relying on the removed
catch-all to swallow the resulting NullPointerException from
MessageDigest.digest(null).

Addresses PR review comment on MtlsClaimsEnhancer.java:135.
The possessive-quantifier change in aff499d only reduced the constant
factor (~3x) for the flagged CodeQL polynomial-regex finding -- it did not
change the underlying O(n^2) complexity, since Matcher.find() retries the
full match attempt at every character position regardless of quantifier
possessive-ness. Independently benchmarked and confirmed: at n=100,000
unmatched '{' characters, the 'fixed' regex still took ~7.5s. The prior
timing test provided no real regression protection (it still passed even
with the original, unfixed regex reverted back in).

The actual fix: bound template length BEFORE it reaches the regex.
ClientAdminEndpointsValidator.validateTlsClientAuthClaimConfig now rejects
any tls-client-auth-sub-template/aud-templates entry exceeding
MAX_TEMPLATE_LENGTH at client creation/update time. MtlsClaimsEnhancer's
renderTemplate independently applies the same bound at token-issuance time
(returning null, i.e. silently dropping the oversized template, consistent
with its existing contract for unresolved placeholders) -- covering
BOSH-flat-config-bootstrapped clients, which bypass admin-API validation
entirely.

Replaces the previous, ineffective timing-based test with a deterministic
functional test asserting oversized templates are rejected/dropped.

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.

Pull request overview

Copilot reviewed 59 out of 59 changed files in this pull request and generated 3 comments.

Suppressed comments (6)

Previously missed (3) — in code that hasn't changed since the last review.

server/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpoints.java:40

  • The discovery response advertises tls_client_auth and an mTLS alias even when uaa.mtls-enabled is at its default false. In that state Tomcat never requests a peer certificate and validators reject mTLS client configuration, so consumers are told an unusable authentication method is supported. Gate both this alias and token_endpoint_auth_methods_supported on the feature flag.
        conf.setMtlsEndpointAliases(Map.of("token_endpoint", contextPath + mtlsEndpointPath));

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java:223

  • Bootstrap clients bypass validateTlsClientAuthClaimConfig, so malformed mappings such as an invalid regex are persisted and later throw during token issuance. The documentation explicitly supports oauth.clients bootstrap configuration; run the same validation used by the admin and zone APIs before registering the client.
            ClientAdminEndpointsValidator.checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, clientId);

docs/UAA-Client-Authentication.md:61

  • This explanation says the dedicated path avoids changing other clients, but MtlsClientAuthTomcatCustomizer sets optionalNoCA on every SSL host config of the connector. TLS happens before an HTTP path is known, so enabling the feature requests an optional client certificate on every HTTPS connection; only application-level processing is path-scoped. Document that connector-wide operational effect.
The client is authenticated on a dedicated endpoint, `/oauth/mtls/token`, rather than the
regular `/oauth/token`. This lets the endpoint be given a servlet-container TLS configuration
that requests a client certificate ("mutual TLS"), without changing behavior for every other
client on `/oauth/token`.

server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthentication.java:235

  • pattern is accepted for every mapping field, but it is ignored for subject_cn and subject_o; additionally, an OU pattern with no capture group is accepted by validation yet silently emits no claim. Apply consistent pattern extraction to all supported fields (or reject patterns for unsupported fields) and require a capture group when a pattern is configured.
            String value = switch (mapping.getField()) {
                case "subject_cn" -> cn;
                case "subject_ou" -> matchFirstOu(ous, mapping.getPattern());
                case "subject_o"  -> extractRdnValue(dn, "O");

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:413

  • Returning immediately when the mappings key is absent also skips validation of dependent settings. A client can therefore persist tls-client-auth-required-claims with no mappings (making authentication permanently fail), or templates with unresolved placeholders (silently dropping sub/aud). Continue validation with an empty declared-claims set so literal templates remain valid while unresolved dependencies are rejected.
    public static void validateTlsClientAuthClaimConfig(Map<String, Object> additionalInfo, String clientId) {
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneEndpointsClientDetailsValidator.java:60

  • Checking only containsKey treats a null or blank tls-client-auth-ca as a usable alternative credential. Such input bypasses the zone API's client_secret requirement, but TlsClientAuthConfiguration.isConfigured later rejects it, leaving an unusable secretless client. Base this decision on a nonblank/configured CA value instead of key presence.

…proxy leaf

isCertificateFromTrustedProxy only performed PKIX path validation on the
genuine TLS peer's certificate (e.g. the Gorouter's backend mTLS cert) --
unlike validateClientCert (fixed in an earlier review round), it never
applied validateEndEntityConstraints afterward. Because the connector's
trust manager accepts any certificate at the TLS layer
(certificateVerification=optionalNoCA), a CA certificate or a certificate
whose Extended Key Usage excludes client authentication could still be
accepted as the trusted XFCC proxy's own credential.

Now calls validateEndEntityConstraints on the validated peer leaf, exactly
mirroring the check already applied in validateClientCert -- integrates
cleanly with the method's existing broad catch-and-log-warn-return-false
handling, no new exception wiring needed.

Addresses PR review comment on TlsClientAuthentication.java:204.
… FIPS

ensureJsseProviderRegistered previously treated the mere presence of a
provider named BCJSSE as sufficient -- if some other, non-FIPS provider had
already been registered under that name (e.g. via JVM-wide java.security
configuration), this method silently kept it, silently defeating the
connector's promised FIPS guarantee.

Now verifies an existing same-named provider is genuinely an instance of
BouncyCastleJsseProvider with isFipsMode() true, throwing
IllegalStateException otherwise rather than silently proceeding.

Addresses PR review comment on MtlsClientAuthTomcatCustomizer.java:109.
Addresses PR review comment on MtlsClientAuthTomcatCustomizer.java:70.
…ror messages

ensureJsseProviderRegistered's fail-fast check previously produced one
generic message ('a different provider is already registered') for both
distinct failure modes -- a wrong provider class entirely, and a genuine
BouncyCastleJsseProvider that simply isn't built in FIPS mode -- printing
the correct class name even in the latter case, which read as contradictory
to an operator debugging a startup failure. Now branches into two clearly
worded IllegalStateExceptions. Adds a test covering the previously-untested
'right class, non-FIPS mode' branch.

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.

Pull request overview

Copilot reviewed 60 out of 60 changed files in this pull request and generated 5 comments.

Suppressed comments (9)

Previously missed (8) — in code that hasn't changed since the last review.

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java:223

  • The documented oauth.clients bootstrap path bypasses validateTlsClientAuthClaimConfig. It can therefore persist invalid fields, regexes, templates, or required-claim references that the admin APIs reject; those values later produce missing claims or token-time failures. Apply the same validation before registering the client.
            ClientAdminEndpointsValidator.checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, clientId);

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:413

  • This early return also accepts tls-client-auth-sub-template, tls-client-auth-aud-templates, or tls-client-auth-required-claims when no mappings key exists. Such templates can never resolve, while required claims make every authentication fail. Reject dependent claim settings unless mappings are present; only a configuration with none of these keys should be a no-op.
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:458

  • A regex with no capture group is accepted here, but matchFirstOu emits a value only when groupCount() >= 1 and then reads group 1. Thus a syntactically valid pattern such as ^app:.*$ is accepted at registration yet silently produces no claim. Either reject patterns without a capture group here or define runtime semantics that use the full match.
                    Pattern.compile(pattern);

server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthentication.java:249

  • pattern is applied only to subject_ou; mappings for subject_cn and subject_o ignore a supplied pattern and copy the complete value. The validator and documentation allow a pattern on every mapping, so accepted CN/O configurations can emit a different claim than configured (and fail required-claim checks). Apply the same matching semantics to all supported fields, or reject patterns for fields where they are unsupported.
            String value = switch (mapping.getField()) {
                case "subject_cn" -> cn;
                case "subject_ou" -> matchFirstOu(ous, mapping.getPattern());
                case "subject_o"  -> extractRdnValue(dn, "O");
                default -> null;

server/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpoints.java:23

  • mtls.endpoint only changes the URL published in discovery; the MVC mapping, security matcher, and servlet-filter path guards remain hard-coded to /oauth/mtls/token. Any non-default value therefore advertises a nonexistent endpoint. Remove this configurability or wire one validated path through every route/matcher.
    @Value("${mtls.endpoint:/oauth/mtls/token}")
    private String mtlsEndpointPath = "/oauth/mtls/token";

docs/UAA-Client-Authentication.md:125

  • This documents token-endpoint-auth-method as required and as selecting mTLS, but the repository has no code that reads this client property; bootstrap merely leaves it as unused additional information, and authentication is selected from the request path plus CA presence. Either implement persistence/enforcement of this metadata or remove the property from the required configuration so operators are not relying on a no-op.
| `token-endpoint-auth-method: tls_client_auth` | yes | Selects mTLS client authentication for this client. |

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:497

  • Null and blank audience templates are silently accepted here, but MtlsClaimsEnhancer calls renderTemplate(template, ...) unconditionally; a null element throws at template.length(), while a blank element can produce an invalid empty aud value. Reject null/blank list entries during client validation.
                for (String template : audTemplates) {
                    if (template != null && !template.isBlank()) {
                        checkTemplateLength(template, TlsClientAuthConfiguration.TLS_CLIENT_AUTH_AUD_TEMPLATES, clientId);
                        validateTemplatePlaceholders(template, declaredClaims,
                                TlsClientAuthConfiguration.TLS_CLIENT_AUTH_AUD_TEMPLATES, clientId);
                    }

docs/UAA-Client-Authentication.md:129

  • The field list omits the supported subject_o value, even though validation and extraction accept it. Operators following this table cannot discover or correctly configure organization-subject mappings; include subject_o in the documented set.
| `tls-client-auth-claim-mappings` | no | List of `{field, pattern, claim}` mappings from certificate subject fields (`subject_cn`, `subject_ou`) to JWT claim names, optionally extracting a capture group via `pattern`. |

model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java:110

  • The copy constructor deletes a flat mTLS CA received through the client-admin API. Because the typed field is @JsonIgnore, deserialization leaves it null and stores tls-client-auth-ca in additionalInformation; line 107 copies that map, then this null setter removes the key. ClientAdminEndpointsValidator constructs this copy before persisting it, so API-created mTLS clients lose their CA configuration. Preserve the copied map when the typed field is null.
            this.setTlsClientAuthConfiguration(uaa.getTlsClientAuthConfiguration());

Comment thread docs/UAA-Client-Authentication.md Outdated
…bled

OpenIdConfiguration's token_endpoint_auth_methods_supported unconditionally
included tls_client_auth, regardless of uaa.mtls-enabled -- a discovery
client on a deployment with mTLS disabled (the default) would select an
authentication method this server cannot actually perform, since such
deployments never request peer certificates at the TLS layer nor allow
tls-client-auth-ca to be configured on any client.

Adds a new 3-arg constructor overload accepting mtlsEnabled, which filters
tls_client_auth out of tokenAMR when false. The existing 2-arg constructor
is unchanged in behavior (delegates with mtlsEnabled=true), preserving all
existing callers/tests.

Addresses PR review comment on OpenIdConfiguration.java:25.
…abled

getOpenIdConfiguration unconditionally set mtls_endpoint_aliases pointing at
/oauth/mtls/token, even on deployments with uaa.mtls-enabled=false (the
default), where that endpoint cannot actually authenticate a client via a
certificate -- making the discovery document contradict the deployment's
actual capabilities.

OpenIdConnectEndpoints now takes a uaa.mtls-enabled-injected constructor
argument, passed through to OpenIdConfiguration's new mtlsEnabled-aware
constructor, and only sets mtls_endpoint_aliases when true.

Addresses PR review comment on OpenIdConnectEndpoints.java:40.
… BouncyCastleFipsProvider

ensureJsseProviderRegistered already verified an existing same-named BCJSSE
provider (an earlier review round), but the BCFIPS crypto-provider
registration just above it still only checked for presence under that name,
not type -- the same provider-name substitution gap. BouncyCastleFipsProvider
has no FIPS/non-FIPS mode distinction (unlike BouncyCastleJsseProvider) --
it IS inherently the FIPS-only crypto provider by construction -- so only an
instanceof check is needed here, not a second mode check.

Addresses PR review comment on MtlsClientAuthTomcatCustomizer.java:119.
…ing invariant

Both discovery gates are driven by the same uaa.mtls-enabled flag today, but
nothing previously asserted they stay consistent with each other -- a future
edit to one call site without the other would silently reintroduce a
contradictory discovery document. Adds an explicit test for both the
enabled and disabled cases.
Previously stated the dedicated /oauth/mtls/token endpoint routing meant
'without changing behavior for every other client on /oauth/token' -- this
conflated endpoint-level authentication scoping (genuinely dedicated) with
the underlying TLS-handshake configuration, which MtlsClientAuthTomcatCustomizer
applies connector-wide via certificateVerification=optionalNoCA. Every TLS
handshake to this UAA instance requests a client certificate when
uaa.mtls-enabled is true, regardless of the endpoint ultimately routed to.

Addresses PR review comment on docs/UAA-Client-Authentication.md:61.
TokenEndpointDocs previously only documented /oauth/token's various grant
types and client authentication methods -- the mTLS token endpoint added by
this PR had no corresponding REST Docs coverage, so generated API
documentation omitted its request format, authentication requirements, and
response shape entirely.

Adds a client_credentials + tls_client_auth documentation test: generates a
CA and a leaf certificate, configures a dedicated documentation client with
tls-client-auth-ca, presents the leaf certificate via the servlet request
attribute the real TLS handshake (or XFCC mapper) would populate, and
documents the resulting token response.

Adds testImplementation(libs.bouncyCastlePkixFips) to uaa/build.gradle.kts
(certificate-generation classes not previously on this module's test
compile classpath), mirroring the existing declaration in
server/build.gradle.kts for the same purpose.

Addresses PR review comment on UaaTokenEndpoint.java:34.
…ndered API docs

TokenEndpointDocs.getTokenUsingClientCredentialGrantWithTlsClientAuth (added
in 1babf25) generates REST Docs snippets, but index.html.md.erb was never
updated to render them -- the generated customer-facing API documentation
would have continued to omit /oauth/mtls/token entirely, the exact gap the
PR review comment on UaaTokenEndpoint.java:34 was raised to close.

Adds a 'Mutual TLS Client Authentication' subsection under Client
Credentials Grant, following the same curl-request/http-request/
http-response/form-parameters/response-fields render() pattern already used
for the Client Secret/Authorization Header/Client Assertion variants, plus
a short prose note and a link to docs/UAA-Client-Authentication.md for
deployment details.
…t tests

a7b77e3/bf6149ae8 made tls_client_auth and mtls_endpoint_aliases only
advertised in the OIDC discovery document when uaa.mtls-enabled is true
(default false), correctly closing a gap where a discovery client could
select an authentication method the server can't actually perform. Three
pre-existing tests in the uaa module -- OpenIdConnectEndpointDocs (a REST
Docs test, which broke the generate-api-docs CI job with a SnippetException
since the documented mtls_endpoint_aliases.token_endpoint field was no
longer present in the default-config response) and
OpenIdConnectEndpointsMockMvcTests/OpenIdConnectEndpointsMockMvcZonePathTests
-- were not updated at the time and started failing under the default
(disabled) configuration once that fix landed, since they assert on
tls_client_auth/mtls_endpoint_aliases being present unconditionally.

Adds @TestPropertySource(properties = "uaa.mtls-enabled=true") to all three,
matching the same pattern already used in TokenEndpointDocs, so these tests
continue to exercise and document the mTLS-enabled discovery document shape
they were originally written for.

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.

Pull request overview

Copilot reviewed 63 out of 63 changed files in this pull request and generated 4 comments.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java:223

  • Bootstrapped oauth.clients bypass the two validators that call validateTlsClientAuthClaimConfig. This path only checks the feature flag, so an invalid regex or null mapping is persisted and later fails token issuance inside certificate-claim extraction. Validate the claim configuration here before registering the client.
            client.setAdditionalInformation(info);
            ClientAdminEndpointsValidator.checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, clientId);

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:413

  • This early return also skips validation when tls-client-auth-required-claims is configured without any claim mappings. Such a client is accepted but can never authenticate because extraction always yields an empty map, so every required claim fails. Treat missing mappings as an empty declaration set and continue validating dependent properties.
    public static void validateTlsClientAuthClaimConfig(Map<String, Object> additionalInfo, String clientId) {
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneEndpointsClientDetailsValidator.java:60

  • Presence of the map key is not equivalent to a configured CA. A blank/null tls-client-auth-ca currently bypasses the required-secret check, so the zone API accepts a secretless client that can never pass TlsClientAuthConfiguration.isConfigured during authentication. Base this decision on a nonblank, valid CA configuration instead.

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:463

  • A syntactically valid pattern with no capture group is accepted here, but matchFirstOu only emits group(1). For example, ^app:.*$ saves successfully yet silently produces no claim (and can make required-claim authentication permanently fail). Reject patterns that do not declare at least one capture group.
            String pattern = mapping.getPattern();
            if (pattern != null && !pattern.isBlank()) {
                try {
                    Pattern.compile(pattern);
                } catch (PatternSyntaxException e) {
                    throw new InvalidClientDetailsException(
                            "tls-client-auth-claim-mappings entry has an invalid pattern '" + pattern
                                    + "' for client_id=" + clientId + ": " + e.getMessage(), e);
                }

Comment on lines +23 to +24
@Value("${mtls.endpoint:/oauth/mtls/token}")
private String mtlsEndpointPath = "/oauth/mtls/token";
Comment on lines +24 to +25
public static final List<String> UAA_SUPPORTED_METHODS =
List.of(CLIENT_SECRET_BASIC, CLIENT_SECRET_POST, NONE, PRIVATE_KEY_JWT, TLS_CLIENT_AUTH);
for deployment and configuration details, including the connector-wide `uaa.mtls-enabled`
requirement.

<%= render('TokenEndpointDocs/getTokenUsingClientCredentialGrantWithTlsClientAuth/curl-request.md') %>

| Property | Required | Description |
|----------|----------|--------------|
| `token-endpoint-auth-method: tls_client_auth` | yes | Selects mTLS client authentication for this client. |
@rkoster
rkoster marked this pull request as draft August 26, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants