Repository navigation
Review of 3972 - #4075
Review of 3972#4075fhanik wants to merge 133 commits into
Conversation
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().
…tailsAuthenticationProvider
…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.
…filter chain Add FilterChainOrder.OAUTH_11 (211) so the mTLS security chain can be ordered before the OAUTH_10 catch-all token endpoint chain. Add mtlsTokenEndpointSecurity @order(OAUTH_11) that: - matches /oauth/mtls/token - runs client-credentials authentication with tls_client_auth support - disables CSRF (stateless machine-to-machine endpoint) - uses BasicAuthenticationEntryPoint so Spring returns 401, not a redirect Without this chain the /oauth/mtls/token path falls through to the LoginSecurityConfiguration catch-all which rejects requests with CSRF-related 403 errors.
… expose /oauth/mtls/token getTlsClientAuthConfiguration previously handled only TlsClientAuthConfiguration (in-memory) and Map (Jackson-deserialized BOSH config stored as nested object). BOSH flat config stores tls-client-auth-ca as a plain PEM string and tls-client-auth-claim-mappings as a JSON array string — add an 'instanceof String pem' branch that parses both. UaaTokenEndpoint @RequestMapping previously covered only /oauth/token. After Gorouter sanitize_set was set, client authentication started succeeding for /oauth/mtls/token but Spring MVC returned 404 because no controller was mapped to that path. Add /oauth/mtls/token to the value array so the same endpoint handles both paths.
…ulti-valued RDNs Two bugs prevented CF identity claims from appearing in tokens issued via /oauth/mtls/token: 1. DB-loaded clients: UaaClientDetails.getTlsClientAuthConfiguration() returns null for clients loaded via JDBC because the tlsClientAuthConfiguration field is only set through JSON deserialization, not through the JDBC row-mapper path (which populates additionalInformation instead). Switch to loadTlsConfig() which mirrors ClientDetailsAuthenticationProvider.getTlsClientAuthConfiguration and reads from additionalInformation directly, handling the TlsClientAuthConfiguration/Map/String cases. 2. Multi-valued RDNs in Diego instance-identity certs: Diego encodes all three OU attributes (app:, space:, organization:) as a single multi-valued RDN using '+' as separator (RFC 2253 §2.2). Splitting only on ',' left the entire '+OU=space:..+OU=org:..' string attached to the app GUID. Fix extractOus() and extractRdnValue() to split on '+' within each RDN component.
- UaaClientDetails.setTlsClientAuthConfiguration() now syncs to additionalInformation so JDBC-loaded clients see the typed value - MtlsClaimsEnhancer checks the typed getTlsClientAuthConfiguration() field first, falls back to loadTlsConfig(additionalInformation) - ClientDetailsAuthenticationProvider and MtlsClaimsEnhancer now handle rawMappings instanceof List<?> (Jackson parses JSON arrays natively from JDBC; not always a String) - Move java-buildpack-client-certificate-mapper-jakarta to Gradle version catalog (libs.versions.toml) - SpringServletXmlFiltersConfiguration: remove class-level @SuppressWarnings and raw FilterRegistrationBean; add comment explaining why reflection is required (ClientCertificateMapper is package-private)
…lient_auth - Add tls_client_auth to tokenAMR assertion in wellKnownEndpoint tests - Assert mtls_endpoint_aliases.token_endpoint in both MockMvc test variants (SUBDOMAIN and ZONE_PATH zone resolution modes) - Document mtls_endpoint_aliases.token_endpoint in REST Docs snippet
…n mTLS auth Thread 8: Replace @JsonProperty(TLS_CLIENT_AUTH_CA) with @JsonIgnore on tlsClientAuthConfiguration in UaaClientDetails. The field is set only programmatically; JSON wire format for tls-client-auth-ca flows through additionalInformation via @JsonAnyGetter/@JsonAnySetter, avoiding the nested serialisation and flat-PEM deserialization failure. Thread 9: Add getCertificateChainFromRequest() returning the full X509Certificate[] from the request attribute. Add chain-aware overload validateClientCert(X509Certificate[], TlsClientAuthConfiguration) that builds CertPath from Arrays.asList(chain) so intermediate CAs are included in PKIX path validation. ClientDetailsAuthenticationProvider now uses the chain-based methods. Single-cert overload kept for backward compat (delegates to chain overload).
After adding @JsonIgnore to UaaClientDetails.tlsClientAuthConfiguration, JSON round-trips populate additionalInformation (via @JsonAnySetter) rather than the typed getter. Update the test to assert on additionalInformation directly, which is the actual contract authentication providers depend on.
…plates win UaaTokenServices.createJWTAccessToken() was unconditionally setting sub and aud after spreading additionalRootClaims, causing MtlsClaimsEnhancer's rendered sub/aud templates to be silently discarded. Move the additionalRootClaims.putAll() to after all UAA-default claims are set (including sub and aud) so that enhancer claims take precedence. Explicit excluded claims are still removed last so operator exclusions win. Add regression test: WhenTokenEnhancerOverridesSubAndAud verifies that an enhancer-supplied sub and aud survive into the final JWT.
9236277 to
f36c1e2
Compare
|
Example fixes here #4076 At this point, I'm not comfortable merging this because it wasn't a pure TDD pattern. Tests were changed along with production code, making it more difficult to review. |
|
Superseded by #4076 |
|
Pushed 14 follow-up commits to #3972, from 471ad57 through a80e1c9, addressing this review and the example fixes in #4076.
38357eb restricts the endpoint to
433728d converts certificate failures to authentication exceptions, yielding consistent 401 /
e4ca968 requires mTLS client configuration at the dedicated endpoint, including descendants and zone-prefixed paths.
343cf61 requires a declared placeholder in nonblank subject templates. Invalid constant/oversized subjects reaching token generation fail issuance. Literal audiences remain supported for federation. Placeholder presence does not guarantee subject uniqueness.
8741802 protects reserved root claims at configuration and enhancement time, including dotted mappings such as
6f3988e explicitly returns 404 before browser security. cb5fbd8 avoids loading or registering the mapper when disabled, verified with the mapper class unavailable.
b3c51f6 restores acceptance as inert metadata; it cannot select or bypass mTLS authentication.
The follow-ups are split by finding. Behavioral fixes were exercised with failing regressions before implementation; refactors used tests run before and after extraction/removal. Tests and production changes are committed together, so history is not a separate red/green sequence. Existing test changes reflect the explicit policies above and new diagnostic messages; positive issuance, ordinary-endpoint, literal-audience, and rotation coverage remains. Targeted suites passed at each step; the latest run passed 17 provider tests, 59 HTTP regressions, and the mTLS REST Docs test. Affected API snippets were regenerated. Full repository/deployment testing was not performed in this round. Full HTML generation remains blocked locally by missing Ruby/gem; pre-existing Markdownlint findings remain. |
…round rkoster pushed 14 follow-up commits to cloudfoundry#3972 (471ad57..a80e1c9) addressing the cloudfoundry#4075 review. 471ad57 is the last commit squashed into this branch's base, so none of those 14 are here -- both branches fixed most of the same findings independently. Comparing them surfaced three gaps on our side. These tests fail today and are made to pass in the following commit. G2 CA rotation is silently broken. A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. Observed: a certificate from the second CA in the bundle is refused with "certificate chain validation failed". G1 pins the first-CA case, which already works, so the pair localises the defect to the anchor set. G3 The mTLS endpoint serves GET. /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. Expects 405 with an Allow: POST header. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other. E7 The buildpack certificate mapper is registered even when mTLS is disabled. It exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. Not covered by a test, and fixed in the following commit: revocation behaviour (setRevocationEnabled(false) -- no CRL, no OCSP) is not mentioned anywhere under docs/.
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed".
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET.
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled.
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed".
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET.
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled.
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed".
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET.
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled.
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed".
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET.
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled.
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed".
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET.
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled.
…red) rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). A platform CA has to be rotatable without an outage, which means publishing a bundle holding the outgoing and incoming CA together. Registration already accepts one -- PemCertificateParser.parseCertificateChain reads every entry -- but TlsClientAuthentication has its own private parser that takes only the first PEM object and builds a single TrustAnchor. So the rotation appears to configure successfully and then half the workloads fail. G1 pins the first-CA case, which already passes today, so the pair localises the defect to the anchor set rather than to bundle parsing generally. G2 fails today: a certificate from the second CA in the bundle is refused with "certificate chain validation failed".
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). /oauth/mtls/token shares a @RequestMapping with /oauth/token and inherits its GET support, so a token request can be made with the parameters in the query string, where access logs and every proxy in front of UAA record them. /oauth/token keeps GET for backwards compatibility; this endpoint is new and has no such debt. G3 fails today: it expects 405 with an Allow: POST header and gets the request served instead. G4 guards the blast radius -- both paths are mapped by the same controller method, so restricting one must not restrict the other; it already passes. Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain, which is what actually lets G3 observe the endpoint's real behaviour instead of whatever Spring Security would otherwise do with an unauthenticated GET.
… (red) rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours diverged before that commit, so port the gap in intent, with our own tests and implementation (next commit). The buildpack certificate mapper exists only to turn Gorouter's X-Forwarded-Client-Cert header into the servlet X509Certificate attribute for this one endpoint, so with the feature off it is a third-party filter in every request's path for no purpose. It is also built reflectively from a package-private class and throws IllegalStateException on failure, which couples UAA's ability to start to that jar being present even for operators who will never enable mTLS. E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is still enabled.
Review notes: RFC 8705 mTLS client authentication
I spent some time on this branch and wrote a set of adversarial MockMvc tests against
/oauth/mtls/tokento check the boundaries rather than reason about them. Everything below is something the tests actually produced, unless marked (from reading).The trust model is carefully built — the XFCC-spoofing defenses in particular are better than I expected, and the
RawPeerCertificateCaptureFilter/ mapper-parse-failure guard is a real subtlety that's handled correctly. The findings below are mostly about the blast radius of the new endpoint rather than the certificate validation itself.What I verified as working
Worth stating up front, because these are the parts under the most load:
tls-client-auth-cais rejected cleanly:401 invalid_client,"tls_client_auth: certificate chain validation failed: Path does not chain with any of the trust anchors". Same for expired certs and a malformed CA PEM.scope,client_idorzid—NON_ADDITIONAL_ROOT_CLAIMSinUaaTokenServicesdoes its job.tls-client-auth-required-claimscorrectly rejects a valid certificate from the same CA in the wrong space. The shared-CA mitigation the docs describe holds.client_secret, can't mix cert+secret on one request, and a CA certificate presented as the leaf is rejected byvalidateEndEntityConstraints.1. The mTLS endpoint serves every grant type, including
passwordandrefresh_tokenPOST /oauth/mtls/tokenwithgrant_type=password, authenticating with a valid instance-identity certificate, returns 200. Decoded access token:{ "client_auth_method": "tls_client_auth", "cnf": { "x5t#S256": "YMmciXQkCQXoo0RJtoqsVM6pH39epZebkh6YnfNVHhk" }, "app_id": "b1-app", "grant_type": "password", "user_id": "d78bfe26-78ca-4400-b44f-15243c9b2e3d", "user_name": "...", "email": "...", "origin": "uaa", "scope": ["uaa.user"] }The refresh token it returns is then usable at the same endpoint (200,
"revocable": true,amr: ["pwd"]).The concern isn't that the client had
passwordin itsauthorized_grant_types— it's what the resulting token claims.cnf.x5t#S256is RFC 8705 §3 sender-constraint: a resource server reading it concludes the presenter holds that certificate. The same token also carries a full user identity. So a Diego app instance certificate ends up certificate-binding a user's token, andapp_id(certificate-derived) sits besideuser_id(password-derived) with nothing marking their provenance.mtlsTokenEndpointSecurityinstallsBackwardsCompatibleTokenEndpointAuthenticationFilterexactly astokenEndpointSecuritydoes, so every grant is reachable. Was that deliberate? If the endpoint is meant for workload identity, restricting it toclient_credentials(and maybejwt-bearer) would match the feature's purpose, and would keepcnfmeaning what RFC 8705 says it means.2. The same validation failure returns 500 with a stack trace, depending on which client-auth filter runs
Authorization: Basic base64("clientId:")— empty secret, which still satisfiesObjectUtils.isEmpty(authentication.getCredentials())inClientDetailsAuthenticationProvider.ClientParametersAuthenticationFilter.wrapClientCredentialLoginskips whenever anAuthorizationheader is present, so this is handled byClientBasicAuthenticationFilter, which catches onlyAuthenticationException:Identical credentials and certificate as the 401 case above — only the filter differs. The clean 401 comes from
AbstractClientParametersAuthenticationFilter.performClientAuthentication'scatch (Exception e) -> BadCredentialsException, which is incidental rather than by design.InvalidClientDetailsExceptionis aUaaException→OAuth2Exception→RuntimeException, not a SpringAuthenticationException, so it escapes the security chain toSecurityFilterChainPostProcessor'sUaaLoggingFilter, which logsERROR "Uncaught Exception:"and sends 500. That's an unauthenticated endpoint writing a stack trace per attempt.Smallest fix is in
validateTlsClientAuth: catch and convert rather than depending on a downstream filter's catch-all.3.
/oauth/mtls/tokenis an unrestricted alias of/oauth/tokenA client with no
tls-client-auth-caat all, authenticating withclient_secret_basicand presenting no certificate, gets a normal token from/oauth/mtls/token(200). Sincemtls_endpoint_aliases.token_endpointnow advertises this path in discovery, it's advertising an endpoint that accepts non-mTLS authentication.4.
tls-client-auth-sub-templatewith no placeholders forges an arbitrarysubMtlsClaimsEnhancer.renderTemplatereturns a template containing no{placeholder}verbatim, andUaaTokenServices.createJWTAccessTokenre-appliessubafter its own defaults.ClientAdminEndpointsValidator.validateTemplatePlaceholdersonly checks that placeholders that are present are declared — it never requires one.Configuring
tls-client-auth-sub-template: "00000000-0000-0000-0000-000000000000"produced a token with exactly thatsub. It needsclients.write, so it's client-admin → subject-spoofing rather than an anonymous hole, butsubis the canonical identity claim for every downstream resource server, anduser_id/cidare protected whilesubdeliberately is not. Requiring at least one placeholder would close it.5. Claim mappings can set
amrandacrNON_ADDITIONAL_ROOT_CLAIMSprotects UAA's own claims but not the authentication-context claims that downstream policy engines read, and the validator applies no allowlist to claim names. An allowlist (or a reserved-name denylist) at config time would be better than relying on the token-builder denylist.6. With
uaa.mtls-enabled=false, the endpoint still resolvesThe security chain is
@ConditionalOnProperty, butUaaTokenEndpoint's@RequestMappinglists/oauth/mtls/tokenunconditionally and both new servlet filters are registered unconditionally. With the feature off, a POST returns:{"error":"Could not verify the provided CSRF token because no token was found to compare."}That's the
uiSecuritycatch-all's CSRF filter — so the request reaches the browser login chain rather than 404ing. It fails closed here, but by accident. A disabled feature's endpoint should not resolve.Related (from reading):
clientCertificateMapperFilter()reflectively instantiates a package-private third-party class at startup unconditionally and throwsIllegalStateExceptionif it's absent — a hard startup failure for a feature that's off.7.
token-endpoint-auth-methodrejection breaks unrelated clientsClientAdminEndpointsValidator.checkMtlsClientConfigAllowedthrows on the mere presence of that key inadditionalInformation, for every client create/update, mTLS or not. Creating a plain client carrying it returns 400. That string appears nowhere else in UAA, but hyphenated unknown keys from a BOSHoauth.clientsmanifest land inadditionalInformation, so deployments may already have clients carrying it — previously ignored, now uncreatable and unupdatable. Is this a leftover from an earlier iteration?Smaller things, from reading rather than testing
MtlsClientAuthTomcatCustomizer.customizeiterates all connectors and allSSLHostConfigs with no port scoping, so enabling the feature makes UAA send aCertificateRequeston every TLS connection including the login page — browsers holding a client cert will prompt end users — and moves the whole connector to BCJSSE in FIPS mode. Worth scoping to a dedicated connector, or documenting loudly.ClientDetailsAuthenticationProvider.getTlsClientAuthConfigurationandMtlsClaimsEnhancer.loadTlsConfigare ~60 lines of identical parsing in different modules. One decides authentication, the other decides what identity claims go in the token; if they drift, those two disagree. A single static factory onTlsClientAuthConfigurationwould be safer.parsePemCertificatedoes oneparser.readObject(), so an operator pasting a root+intermediate bundle intotls-client-auth-casilently gets only the first certificate as trust anchor.setRevocationEnabled(false)is reasonable for short-lived instance certs but deserves a line in the docs, since the same config accepts arbitrary long-lived certificates.allowQueryStringForTokens(default true), soGET /oauth/mtls/token?grant_type=...works.ClientCredentialsTokenGranter.isAllowedAuthMethodis public static and called only from a test.Offer
I have the MockMvc tests that produced all of the above — roughly 17 tests covering credential confusion, grant types at the endpoint, validation-failure semantics, and claim-mapping reach, using real BouncyCastle-generated certificates rather than mocks. Happy to push them to the branch if useful; several of them are worth keeping as regression coverage regardless of what you decide about the findings, particularly the wrong-CA case and the protected-claim guard, which aren't currently covered.