Skip to content

Add per-IdP CA certificate trust for OIDC/OAuth2, LDAP, and SAML - #4019

Merged
duanemay merged 7 commits into
developfrom
per-idp-certs
Aug 13, 2026
Merged

Add per-IdP CA certificate trust for OIDC/OAuth2, LDAP, and SAML#4019
duanemay merged 7 commits into
developfrom
per-idp-certs

Conversation

@duanemay

Copy link
Copy Markdown
Member

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 without
requiring the CA to be installed into the JVM's global truststore at deploy time.

  • New caCertificates field (list of PEM-encoded certs) on OIDCIdentityProviderDefinition /
    RawExternalOAuthIdentityProviderDefinition, LdapIdentityProviderDefinition, and
    SamlIdentityProviderDefinition, validated at save time.
  • A new shared IdpOutboundTrustCache, built on a per-node Caffeine cache, that builds a merged
    trust context (JDK default trust anchors + the IdP's supplied CA certs) once per identity and
    reuses it, rather than rebuilding per request.
  • OIDC/OAuth2: OidcMetadataFetcher and ExternalOAuthAuthenticationManager resolve their
    RestTemplate through the shared cache for discovery, token, userinfo, and JWKS calls.
  • LDAP: a new CaCertAwareLdapSocketFactory resolves trust dynamically per createSocket()
    call from the current IdentityZoneHolder zone, since JNDI caches its getDefault() singleton
    across 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.
  • SAML: FixedHttpMetaDataProvider resolves trust through the shared cache for metadata-URL
    fetches, keyed by the IdP's existing unique alias.
  • Fixes a latent cross-IdP cache-poisoning risk in the shared URL content cache (used by both OIDC
    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.
  • In all three connectors, absent caCertificates keeps existing behavior unchanged, and
    skipSslValidation still takes precedence when both are set.

Test plan

  • Unit tests for the shared IdpOutboundTrustCache and PemCertificateParser, including a
    concurrency test proving atomic per-key cache updates under contention
  • Unit tests for each connector's validation and trust-resolution wiring
  • CaCertAwareLdapSocketFactoryTest spins up a real local TLS server and proves two zones on
    the same host with different caCertificates don't leak trust between each other
  • ConfiguratorRelyingPartyRegistrationRepositoryTest spins up a real local HTTPS server and
    proves SAML registration resolution fails without caCertificates and succeeds with it
  • Manually verified end-to-end locally against a running UAA instance for OIDC and LDAP: created
    an 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
  • All existing tests in touched files continue to pass unmodified

Copilot AI review requested due to automatic review settings July 31, 2026 15:44

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

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 caCertificates on OIDC/OAuth2, LDAP, and SAML IdP definitions with save-time validation via PemCertificateParser.
  • Adds IdpOutboundTrustCache to cache merged-trust SSLContext / RestTemplate instances 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.

Comment thread server/src/main/java/org/cloudfoundry/identity/uaa/util/PemCertificateParser.java Outdated
duanemay added 4 commits July 31, 2026 13:04
…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.
duanemay and others added 3 commits July 31, 2026 14:04
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>

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 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.

Comment on lines +34 to +36
private static final IdpOutboundTrustCache TRUST_CACHE = new IdpOutboundTrustCache();
private static final ConcurrentMap<String, ZoneLdapTrust> ZONE_TRUST = new ConcurrentHashMap<>();

@duanemay
duanemay requested review from fhanik and strehle August 12, 2026 15:13
@github-project-automation github-project-automation Bot moved this from Inbox to Pending Merge | Prioritized in Foundational Infrastructure Working Group Aug 13, 2026
@duanemay
duanemay merged commit c0c035d into develop Aug 13, 2026
27 checks passed
@duanemay
duanemay deleted the per-idp-certs branch August 13, 2026 22:15
@github-project-automation github-project-automation Bot moved this from Pending Merge | Prioritized to Done in Foundational Infrastructure Working Group Aug 13, 2026
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.

4 participants