Example fixes for 3792 - #4076
Draft
fhanik wants to merge 134 commits into
Draft
Example fixes for 3792 #4076fhanik wants to merge 134 commits into
fhanik wants to merge 134 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.
…client-auth # Conflicts: # server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneEndpointsClientDetailsValidator.java
Draft
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Harden the RFC 8705 mTLS token endpoint
Seven patches closing six security weaknesses and one regression, each backed by a
failing test in
MtlsTokenEndpointHardeningMockMvcTests/MtlsDisabledTokenEndpointMockMvcTests.1. Why test code changed
Changing tests alongside a fix is normally a smell — it's how a red test gets argued
into green without the defect being fixed. Three test files change here. None of them
weakens an assertion about behaviour under test, and the distinction matters enough to
itemise.
1.1 A pre-existing test asserted the absence of the rule being added
ClientAdminEndpointsValidatorTests.validateTlsClientAuthClaimConfig_acceptsSubTemplateAtExactlyMaxLengthfed the validator a 256-character all-
{template and assertedassertThatNoException().That test's purpose is a ReDoS timing bound — its own comment says so, and its real
assertion is
elapsedMillis < 100. The pathological input it uses happens to contain noclosing brace, so it also contains no placeholder. This patch rejects placeholder-free
templates, which means that input is now invalid for a second, unrelated reason.
The test was updated to assert the rejection arrives quickly, and renamed to
validateTlsClientAuthClaimConfig_rejectsPlaceholderlessSubTemplateAtMaxLengthQuickly.The timing bound — the thing the test exists to protect — is unchanged. What changed is
the expected outcome for one input, because the patch deliberately changes it. A test
that pins behaviour a patch intentionally alters has to move with the patch; the
alternative is keeping a rule the test forbids.
No other author test changed.
MtlsClaimsEnhancerTest,TlsClientAuthenticationTest,ClientDetailsAuthenticationProviderTestsandUaaTokenEndpointTestsare untouched —including
UaaTokenEndpointTests, which is whydoDelegateGetkeeps its two-argumentsignature and the servlet request is resolved from
RequestContextHolderinstead.1.2 One of my own assertions over-specified the fix
B1's refusal branch assertederror == "invalid_client". The security property is"the mTLS endpoint must not issue a certificate-bound user token" — but that assertion
had quietly encoded a second requirement: "and it must be refused as a
client-authentication failure."
A grant type the endpoint does not serve is
invalid_grant, notinvalid_client. Myassertion had prejudged which layer the fix belonged in. It now accepts any 4xx carrying
an OAuth error code, and still fails on a 5xx or a body with no error code — so the thing
it actually guards against (an ugly failure) is intact, while the implementation choice
is no longer constrained by the test.
This is my bug, introduced before the fix existed, and it is the only assertion in the
suite that was loosened.
1.3 New servlet filter needs wiring into the MockMvc chain
DefaultTestContextbuildsMockMvcwith exactly three filters: zone-path rewriting,zone session, and the Spring Security chain. A filter registered in
SpringServletXmlFiltersConfigurationdoes not appear there automatically —TokenEndpointDocsalready has to addrawPeerCertificateCaptureFilterby hand for thesame reason, with a comment saying so.
MtlsDisabledTokenEndpointMockMvcTestsnow builds its ownMockMvcincludingmtlsEndpointAvailabilityFilter. That is harness wiring, not an assertion: without itthe filter under test never executes, and E1 would pass or fail for reasons unrelated to
the patch.
1.4 What did not change
The assertions expressing the seven findings are byte-identical to the versions that were
red before this commit. A3, C5, D2, D3, E1 and E4 assert exactly what they asserted then.
If a patch is wrong, its test still fails.
2. Backwards compatibility
Every patch is gated on a condition that is false for all pre-existing traffic. Two are
gated on the request path, three on per-client mTLS configuration, one is deny-only on a
single path, and one strictly widens what the API accepts.
2.1 Patches gated on the mTLS request path
ClientDetailsAuthenticationProvider's new guard andUaaTokenEndpoint's grantrestriction both begin with a path test:
isMtlsTokenPathmatches/oauth/mtls/tokenand paths beneath it, and nothing else. For/oauth/token,/oauth/token/alias/{entityId},/oauth/authorize, the SCIM endpoints,the login UI and every other route, both patches are a no-op before they read anything.
client_secret_basic,client_secret_post,private_key_jwt,none, PKCE, SAMLbearer, JWT bearer and refresh flows are not reachable from either branch.
The
else if (isTlsClientAuthPath(...))branch removed from the empty-credentials blockwas itself reachable only on that same path, and only for clients with no
tls-client-auth-ca— exactly the case the new guard now rejects earlier. Nothingoutside
/oauth/mtls/tokencan reach either.2.2 Patches gated on per-client mTLS configuration
validateTlsClientAuth's newcatchis reached only from the branch guarded byTlsClientAuthConfiguration.isConfigured(...)— a client with notls-client-auth-canever enters it. The converted exception keeps
e.getMessage()verbatim, so even formTLS clients the HTTP response body is unchanged on the path that already returned 401;
only the path that returned 500 moves.
ClientAdminEndpointsValidator.validateTlsClientAuthClaimConfigreads only keys prefixedtls-client-auth-. A client carrying none of them reaches no new code:requireAtLeastOnePlaceholderruns only for a non-blank template that exists, and theRESERVED_CLAIM_NAMEScheck only for entries intls-client-auth-claim-mappings.MtlsClaimsEnhancer.enhancealready returned an empty map unless the request carried acertificate and the authentication method was
tls_client_authand the client hadTLS config. The reserved-name skip and the placeholder check sit inside that path.
These three do change behaviour for mTLS clients configured with a placeholder-free
template or a mapping onto a reserved claim — that is the point.
tls_client_authshipsfor the first time in this PR, so no released deployment has such a client to break.
RESERVED_CLAIM_NAMESis astatic finalconstant onTlsClientAuthConfiguration, notan instance field.
UaaClientDetailsandTlsClientAuthConfigurationserialise exactlyas before; no stored client JSON changes shape, and no migration is implied.
2.3 The new filter is deny-only and scoped to one path
MtlsEndpointAvailabilityFilterruns on every request, which is worth being explicitabout. Its entire body is:
It has no branch that grants anything. When
uaa.mtls-enabledis true it is apass-through for all paths. When false it affects only
/oauth/mtls/token, which todayanswers
403 "Could not verify the provided CSRF token"from theuiSecuritycatch-all— no working flow depends on that response. Cost on every other request is one string
comparison.
Order
-290places it afterZonePathContextRewritingFilter(
Ordered.HIGHEST_PRECEDENCE + 1), so/z/{subdomain}/oauth/mtls/tokenis matchedidentically to a direct request, and before Spring Security (
-100), so the path neverreaches a filter chain. It does not displace
rawPeerCertificateCaptureFilter(-300)or
clientCertificateMapperFilter(-200); the relative order of those two, and theirrelationship to the security filter, is unchanged.
2.4 The one patch that touches every client widens what is accepted
Removing the
token-endpoint-auth-methodrejection fromcheckMtlsClientConfigAllowedis the only change on a code path every client create and update traverses. It deletes a
validation; it adds none. A client that was accepted before is still accepted, and
clients carrying that key — silently stored and ignored by UAA for years, and rejected
only by this PR — can be created and updated again. Nothing in the codebase reads the key.
2.5 Not changed
Certificate validation itself is untouched: PKIX path building,
validateEndEntityConstraints, the XFCC trusted-proxy checks,RawPeerCertificateCaptureFilter,MtlsPathGuardedFilter, the Tomcat/BCJSSE connectorcustomiser and
required-claimsenforcement are all as the author wrote them. So areUaaTokenServices,ClientCredentialsTokenGranter, the OIDC discovery changes and theClientAuthenticationconstants.3. The patches
/oauth/mtls/tokenserved any client with a validclient_secret, certificate or not — while OIDC discovery advertises it asmtls_endpoint_aliases.token_endpointClientDetailsAuthenticationProviderrejects any client withouttls-client-auth-caon that path, whatever credentials it presentspasswordgrant there, producing one token carrying bothcnf.x5t#S256(RFC 8705 §3 sender-constraint) anduser_id/user_name/email— two unrelated identities, with the returned refresh token usable at the same endpointUaaTokenEndpointserves onlyclient_credentialson that path, rejected asinvalid_grant, without echoing the submitted grant typeInvalidClientDetailsExceptionis aUaaException→OAuth2Exception→RuntimeException, so it escapedClientBasicAuthenticationFilter(which catches onlyAuthenticationException): the same untrusted certificate returned 401 with aclient_idparameter and 500 plus an ERROR stack trace withAuthorization: Basic clientId:— an unauthenticated log-flooding vectorBadCredentialsExceptionat the throw site, message preservedtls-client-auth-sub-templatewith no{placeholder}renders to itself, andUaaTokenServicesre-appliessubafter its defaults — so a constant template setsubto any fixed string, including a real user's idMtlsClaimsEnhancer.renderTemplatedrops placeholder-free templates as defense in depth for the bootstrap path, which bypasses admin-API validationNON_ADDITIONAL_ROOT_CLAIMSprotects UAA's own claims but notamr,acr,auth_time,client_auth_methodorcnf, and the validator applied no allowlist to claim names — so a certificate subject field could assert, in a signed token, how the caller authenticatedTlsClientAuthConfiguration.RESERVED_CLAIM_NAMES; validator rejects mappings onto them, enhancer skips themuaa.mtls-enabled=falsethe security chain is absent but the@RequestMappingis not, so the path fell through to the browser login chain and was stopped by CSRF rather than by any gateMtlsEndpointAvailabilityFilterreturns 404 for that path when the feature is offcheckMtlsClientConfigAllowedrejected every client create/update carryingtoken-endpoint-auth-method— not a UAA property, not read anywhere, and reachable via hyphenated BOSH manifest keys4. Known gaps not addressed
Deliberately out of scope, to keep the commit to its tests:
GET /oauth/mtls/token?grant_type=...is still permitted by the inheritedallowQueryStringForTokensdefault.clientCertificateMapperFilterstill reflectively instantiates a package-privatethird-party class at startup even when the feature is disabled.
validateTlsClientAuthstill returns a barefalse, so "no certificate presented" and"trusted certificate, wrong
required-claims" produce byte-identical denials —A4and
D4both pintls_client_auth: certificate validation failed.