Add per-IdP CA certificate trust for OIDC/OAuth2, LDAP, and SAML - #4019
Conversation
There was a problem hiding this comment.
Pull request overview
Adds per–identity-provider outbound TLS trust configuration (via a caCertificates PEM list) for OIDC/OAuth2, LDAP, and SAML connectors, backed by a shared per-node cache so merged trust material is built once per IdP identity and reused.
Changes:
- Introduces
caCertificateson OIDC/OAuth2, LDAP, and SAML IdP definitions with save-time validation viaPemCertificateParser. - Adds
IdpOutboundTrustCacheto cache merged-trustSSLContext/RestTemplateinstances and wires it into OIDC discovery/JWKS, OAuth flows, LDAP socket creation, and SAML metadata fetches. - Prevents cross-IdP cache poisoning by bypassing shared URL content caches whenever per-IdP custom trust material is in play.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtilsTest.java | Adds tests for new SSLContext-based request-factory overload behavior. |
| server/src/test/java/org/cloudfoundry/identity/uaa/util/PemCertificateParserTest.java | New unit tests for PEM parsing and indexed error reporting. |
| server/src/test/java/org/cloudfoundry/identity/uaa/security/IdpOutboundTrustCacheTest.java | New tests for cache behavior, merged trust manager, and concurrency safety. |
| server/src/test/java/org/cloudfoundry/identity/uaa/security/CaCertAwareLdapSocketFactoryTest.java | New integration-style test proving per-zone LDAP trust isolation. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderConfiguratorTests.java | Updates mocks/wiring for new SAML metadata fetch signature + trust cache injection. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/Saml2BearerGrantAuthenticationConverterTest.java | Updates SAML configuration wiring to pass IdpOutboundTrustCache. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/FixedHttpMetaDataProviderTest.java | New unit tests for SAML metadata fetch cache-bypass behavior under custom trust. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/ConfiguratorRelyingPartyRegistrationRepositoryTest.java | Adds real-HTTPS test verifying SAML URL metadata fetch succeeds only when caCertificates is set. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/BootstrapSamlIdentityProviderDataTests.java | Updates mocks for new SAML metadata fetch signature. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/OidcMetadataFetcherTest.java | Adds tests ensuring OIDC discovery/JWKS bypass shared cache when caCertificates is set. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidatorTest.java | Adds caCertificates validation tests for external OAuth/OIDC definitions. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTrustCacheTest.java | New unit tests for ExternalOAuthAuthenticationManager trust-cache delegation. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java | Updates overridden method signature to match new RestTemplate resolution API. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/ProcessLdapPropertiesTest.java | Adds tests for selecting CA-cert-aware LDAP socket factory and precedence with skip-SSL. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderDefinitionTest.java | Ensures LDAP caCertificates impacts equality/hash and environment flagging. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderConfigValidatorTest.java | Adds caCertificates validation tests for LDAP definitions. |
| server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderConfigValidationDelegatorTest.java | Adds SAML caCertificates validation coverage in the delegator path. |
| server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java | Adds SSLContext-based request-factory / HttpClientBuilder path with hostname verification enabled. |
| server/src/main/java/org/cloudfoundry/identity/uaa/util/PemCertificateParser.java | New utility for parsing PEM X.509 certificates (used by validators and trust cache). |
| server/src/main/java/org/cloudfoundry/identity/uaa/util/LdapUtils.java | Propagates ldap.ssl.hasCaCertificates into the LDAP configuration environment. |
| server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlBeansConfiguration.java | Wires OIDC metadata fetcher to use shared trust cache + RestTemplateConfig. |
| server/src/main/java/org/cloudfoundry/identity/uaa/security/IdpOutboundTrustCache.java | New shared Caffeine-backed cache to build/retain merged-trust SSL contexts and RestTemplates. |
| server/src/main/java/org/cloudfoundry/identity/uaa/security/CaCertAwareLdapSocketFactory.java | New LDAP socket factory that resolves trust dynamically per zone for JNDI singleton constraints. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderConfigurator.java | Passes per-IdP CA certs and identity key into SAML metadata URL fetch. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlConfiguration.java | Injects shared trust cache into FixedHttpMetaDataProvider construction. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/FixedHttpMetaDataProvider.java | Uses trust cache for metadata URL fetch and bypasses shared URL cache under custom trust. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OidcMetadataFetcher.java | Routes OIDC discovery/JWKS fetch through trust cache and bypasses shared cache under custom trust. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java | Validates external OAuth/OIDC caCertificates at save time via PEM parsing. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java | Resolves outbound RestTemplate via trust cache keyed by IdP id. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/ldap/ProcessLdapProperties.java | Selects CA-cert-aware LDAP socket factory when hasCaCertificates is set. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderConfigValidator.java | Validates LDAP caCertificates using PEM parser. |
| server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderConfigValidationDelegator.java | Adds SAML caCertificates validation in the delegator (SAML lacks a dedicated validator). |
| server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java | Injects shared trust cache + RestTemplateConfig into ExternalOAuthAuthenticationManager bean. |
| server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/RestTemplateConfig.java | Exposes shared IdpOutboundTrustCache as a Spring bean. |
| server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/LdapIntegrationConfig.java | Wires ldap.ssl.hasCaCertificates into ProcessLdapProperties construction. |
| server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java | Registers per-zone LDAP trust inputs into CaCertAwareLdapSocketFactory on (re)build. |
| model/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java | Adds SAML definition serialization/clone tests for caCertificates. |
| model/src/test/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinitionTests.java | Adds OIDC definition serialization/equals/hash tests for caCertificates. |
| model/src/main/java/org/cloudfoundry/identity/uaa/provider/SamlIdentityProviderDefinition.java | Adds caCertificates field and ensures clone copies it. |
| model/src/main/java/org/cloudfoundry/identity/uaa/provider/LdapIdentityProviderDefinition.java | Adds LDAP caCertificates + env flag constant + equals/hash integration. |
| model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractExternalOAuthIdentityProviderDefinition.java | Adds OAuth/OIDC caCertificates field and includes it in equals/hash. |
…lities - Added `IdpOutboundTrustCache` to build and cache TLS trust contexts for identity providers. - Introduced `PemCertificateParser` for parsing and validating PEM-encoded certificates. - Extended `UaaHttpRequestUtils` to support SSL context injection for HTTP clients. - Added unit tests for new utilities and edge cases.
Wires the shared per-IdP TLS trust cache into the OIDC/OAuth2 connector: a new caCertificates field on external OAuth IdP definitions, validated at save time, and consulted for discovery, token, userinfo, and JWKS calls so an IdP's private CA can be trusted without disabling TLS validation entirely. Absent caCertificates keeps today's behavior unchanged, and skipSslValidation still takes precedence when both are set. Also fixes a latent cache-poisoning risk in the shared URL content cache by bypassing it whenever a per-IdP trust context is in play. � Conflicts: � server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlBeansConfiguration.java � server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/RestTemplateConfig.java � server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OidcMetadataFetcher.java
Wires the shared per-IdP TLS trust cache into the LDAP connector: a new caCertificates field on LdapIdentityProviderDefinition, validated at save time, and consulted by a new CaCertAwareLdapSocketFactory for ldaps:// and StartTLS connections. The LDAP case is trickier than OIDC/OAuth2 because JNDI resolves the socket factory via a reflective getDefault() call and caches that one singleton across every zone's LDAP connections for the life of the JVM -- a fixed trust decision baked into the constructor (as the existing LdapSocketFactory/SkipSslLdapSocketFactory do) would mean whichever zone loaded first wins forever. Instead trust is resolved per createSocket() call from the current zone (IdentityZoneHolder) against a small static registry, populated by DynamicLdapAuthenticationManager whenever a zone's LDAP config is (re)loaded. Absent caCertificates keeps today's behavior unchanged, and skipSslValidation still takes precedence when both are set.
Wires the shared per-IdP TLS trust cache into the SAML connector: a new caCertificates field on SamlIdentityProviderDefinition, validated inline in IdentityProviderConfigValidationDelegator (SAML has no dedicated validator class, unlike OAuth/LDAP/UAA), and consulted by FixedHttpMetaDataProvider for metadata-URL fetches. Unlike OIDC, SAML metadata fetch isn't gated behind a separate discovery step -- it's exercised live both at IdP create/update validation time and on essentially every login/registration-resolution call for URL-type IdPs, so the per-IdP trust lookup had to stay cheap on that hot path. Uses the IdP's existing unique alias (entity alias + zone id) as the trust cache key, already a stable, always-populated identifier on this definition. Also fixes the same shared-URL-cache cross-IdP poisoning risk addressed for OIDC, and preserves SAML's own distinct socket timeout configuration when falling back to the existing global trust behavior. Absent caCertificates keeps today's behavior unchanged, and skipSslValidation still takes precedence when both are set.
Eagerly initialize the CaCertAwareLdapSocketFactory singleton instead of lazy-initializing without synchronization, and make PemCertificateParser decode PEM input with an explicit UTF-8 charset and reject null/blank entries with a clear validation error instead of a low-level NPE. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The caCertificates field added to OIDC/OAuth2, LDAP, and SAML identity provider configs was missing from the REST Docs field descriptors, which failed the docs snippet generation for SAML and LDAP (their definitions serialize the field even when null, unlike the OAuth/OIDC config which suppresses null via @JsonInclude). Add a shared field descriptor and wire it into all four provider doc field lists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…LDAP caCertificates previously only worked for identity providers managed through the /identity-providers REST API, since the JSON body is deserialized directly onto the definition class. The YAML-bootstrap paths (login.saml.providers, login.oauth.providers, and the ldap.* single-tenant config) each hand-map individual fields from a raw Map<String,Object> and were missing this one, so operators configuring an IdP via uaa.yml had no way to set it. Add the field to each of the three manual mappings and document it in the configuration reference. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 48 out of 48 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
server/src/test/java/org/cloudfoundry/identity/uaa/security/CaCertAwareLdapSocketFactoryTest.java:92
- Same as above: explicitly starting the TLS handshake makes the test reliably validate that the trust decision is correct when reconnecting under the original zone.
server/src/main/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderConfigValidator.java:46 - The thrown IllegalArgumentException message concatenates without a separator and drops the original cause, which makes PEM validation failures harder to understand/debug. Prefer a clearer message with punctuation and preserve the original exception as the cause.
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid config for Identity Provider " + e.getMessage());
}
server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderConfigValidationDelegator.java:79
- This wraps PEM parsing errors in a generic message without context (no separator, no originKey), and it drops the original cause. Including the provider originKey and preserving the cause makes it much easier to identify which SAML IdP config is invalid.
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid config for Identity Provider " + e.getMessage());
}
server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtilsTest.java:274
- Asserting directly on
e.getCause().getClass()is brittle (the handshake exception is often wrapped) and can NPE if the cause chain changes. Use an AssertJ root-cause assertion instead so the test is stable across HTTP client / JDK variations.
server/src/test/java/org/cloudfoundry/identity/uaa/security/CaCertAwareLdapSocketFactoryTest.java:74 - This success-path assertion doesn't explicitly complete the TLS handshake, so it may not reliably prove that the per-zone trust material was actually used (depending on when JSSE initiates handshakes). Explicitly start the handshake before asserting session validity.
This issue also appears on line 90 of the same file.
| private static final IdpOutboundTrustCache TRUST_CACHE = new IdpOutboundTrustCache(); | ||
| private static final ConcurrentMap<String, ZoneLdapTrust> ZONE_TRUST = new ConcurrentHashMap<>(); | ||
|
|
Summary
Adds an API-driven way to trust a private CA for a specific identity provider's outbound TLS
connections, without disabling certificate validation entirely (
skipSslValidation) and withoutrequiring the CA to be installed into the JVM's global truststore at deploy time.
caCertificatesfield (list of PEM-encoded certs) onOIDCIdentityProviderDefinition/RawExternalOAuthIdentityProviderDefinition,LdapIdentityProviderDefinition, andSamlIdentityProviderDefinition, validated at save time.IdpOutboundTrustCache, built on a per-node Caffeine cache, that builds a mergedtrust context (JDK default trust anchors + the IdP's supplied CA certs) once per identity and
reuses it, rather than rebuilding per request.
OidcMetadataFetcherandExternalOAuthAuthenticationManagerresolve theirRestTemplatethrough the shared cache for discovery, token, userinfo, and JWKS calls.CaCertAwareLdapSocketFactoryresolves trust dynamically percreateSocket()call from the current
IdentityZoneHolderzone, since JNDI caches itsgetDefault()singletonacross every zone's LDAP connections for the life of the JVM — a fixed trust decision baked into
the constructor (as the existing socket factories do) would mean whichever zone loaded first wins
forever.
FixedHttpMetaDataProviderresolves trust through the shared cache for metadata-URLfetches, keyed by the IdP's existing unique alias.
discovery and SAML metadata fetch): it keyed purely on URL, not on which trust context fetched it,
so two IdPs sharing a URL with different CAs could have silently shared a cached response once
per-IdP trust existed. Both are now bypassed whenever a custom CA is in play.
caCertificateskeeps existing behavior unchanged, andskipSslValidationstill takes precedence when both are set.Test plan
IdpOutboundTrustCacheandPemCertificateParser, including aconcurrency test proving atomic per-key cache updates under contention
CaCertAwareLdapSocketFactoryTestspins up a real local TLS server and proves two zones onthe same host with different
caCertificatesdon't leak trust between each otherConfiguratorRelyingPartyRegistrationRepositoryTestspins up a real local HTTPS server andproves SAML registration resolution fails without
caCertificatesand succeeds with itan IdP pointed at a private-CA-signed endpoint, confirmed the login flow fails with the
expected TLS error without
caCertificates, and succeeds once it's set