Skip to content

feat: add tls client auth idp brokering (mtls) (#38310) - #51474

Open
eryx12o45 wants to merge 1 commit into
keycloak:mainfrom
eryx12o45:feature/38310-tls-client-auth-idp-brokering-fixed
Open

feat: add tls client auth idp brokering (mtls) (#38310)#51474
eryx12o45 wants to merge 1 commit into
keycloak:mainfrom
eryx12o45:feature/38310-tls-client-auth-idp-brokering-fixed

Conversation

@eryx12o45

@eryx12o45 eryx12o45 commented Aug 6, 2026

Copy link
Copy Markdown

Closes #38310

Disclaimer: Generative AI was used to support the creation of this PR, but only to support the work.

Thanks @newtork for your review, changes and support

Copilot AI balanced review requested due to automatic review settings August 6, 2026 06:26
@eryx12o45
eryx12o45 requested review from a team as code owners August 6, 2026 06:26

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 RFC 8705 tls_client_auth support for OIDC identity-provider brokering.

Changes:

  • Adds realm-key-based client certificate selection and mTLS endpoint aliases.
  • Extends outbound HTTP clients with per-request TLS key material and lifecycle handling.
  • Adds Admin UI, documentation, and automated tests.

Reviewed changes

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

Show a summary per file
File Description
tests/base/.../IdentityProviderOidcTlsClientAuthTest.java Tests Admin API validation.
services/.../HttpClientBuilderTest.java Tests retained key managers.
services/.../DefaultHttpClientFactoryTest.java Tests dedicated-client settings.
services/.../OIDCIdentityProviderFactoryTest.java Tests discovery aliases.
services/.../OAuth2IdentityProviderConfigTest.java Tests mTLS configuration.
services/.../IdpMtlsSslContextProviderTest.java Tests SSL-context construction.
services/.../IdpClientCertificateResolverTest.java Tests certificate resolution.
services/.../HttpClientBuilder.java Supports custom key managers.
services/.../DefaultHttpClientFactory.java Builds dedicated TLS clients.
services/.../OIDCIdentityProviderFactory.java Imports mTLS aliases.
services/.../OIDCIdentityProviderConfig.java Uses session-aware validation.
services/.../OIDCIdentityProvider.java Routes backchannel requests through mTLS.
services/.../OAuth2IdentityProviderConfig.java Adds mTLS settings and validation.
services/.../IdpMtlsSslContextProvider.java Builds client TLS material.
services/.../IdpClientCertificateResolver.java Resolves realm certificates.
services/.../AbstractOAuth2IdentityProvider.java Authenticates backchannel requests with mTLS.
server-spi/.../IdentityProviderModel.java Adds session-aware validation API.
server-spi-private/.../SimpleHttpOwnedClientTest.java Tests owned-client closure.
server-spi-private/.../RepresentationToModel.java Invokes session-aware validation.
server-spi-private/.../SimpleHttpResponse.java Runs response completion callbacks.
server-spi-private/.../SimpleHttpRequest.java Closes owned clients.
server-spi-private/.../SimpleHttp.java Adds client completion policy.
server-spi-private/.../HttpClientProvider.java Exposes dedicated-client APIs.
js/.../OIDCAuthentication.tsx Adds mTLS configuration controls.
js/.../messages_en.properties Adds mTLS UI labels.
docs/.../identity-broker/oidc.adoc Documents broker mTLS configuration.
docs/.../26_7_0.adoc Adds release notes.

Comment thread js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx
Comment thread docs/documentation/server_admin/topics/identity-broker/oidc.adoc Outdated
Copilot AI review requested due to automatic review settings August 6, 2026 07:22
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 4bb95fa to 0020f71 Compare August 6, 2026 07:22

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 27 out of 27 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:68

  • The /keys response includes disabled keys, while IdpClientCertificateResolver accepts only ACTIVE/PASSIVE keys. This selector therefore offers choices that are guaranteed to be rejected on save; filter out disabled or missing statuses when building the options.
      if (k.certificate && k.providerId && !seen.has(k.providerId)) {

Copilot AI review requested due to automatic review settings August 6, 2026 08:48
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 0020f71 to 6234bba Compare August 6, 2026 08:48

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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (4)

services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java:310

  • Replacing the existing validate(RealmModel) override makes that still-public validation entry point a no-op for OAuth2IdentityProviderConfig (and strips common URL/PKCE checks from subclasses or extensions that call it directly). Keep the realm-only override for validations that do not need a session, and have the new overload delegate to it before adding the key-resolution check; OIDCIdentityProviderConfig should likewise retain its realm-only checks.
    @Override
    public void validate(KeycloakSession session, RealmModel realm) {
        // Invoke the legacy validate(RealmModel) hook first so provider-specific checks registered by
        // subclasses that only override the (RealmModel) overload keep running (e.g. the generic OAuth

services/src/main/java/org/keycloak/broker/oidc/mtls/IdpClientCertificateResolver.java:32

  • This accepts any non-null Key, but buildKeyManagers later requires a PrivateKey; a custom key provider can therefore pass create/update validation and then fail every mTLS request. Validate the same type requirement here so unusable configurations are rejected up front.
        if (key.getPrivateKey() == null) {
            throw new IllegalStateException(
                    "Realm key " + keyProviderId + " has no private key; cannot be used for tls_client_auth.");
        }

model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java:383

  • Moving all components ahead of groups breaks imports containing an LDAP group mapper with a custom groups path: GroupLDAPStorageMapperFactory.validateConfiguration resolves that path during importComponentModel and rejects it because groups are not imported until line 474. Import only the key-provider components needed by IdP validation here, and leave other components at their previous post-group position.
        if (rep.getComponents() != null) {
            MultivaluedHashMap<String, ComponentExportRepresentation> components = rep.getComponents();
            String parentId = newRealm.getId();
            importComponents(newRealm, components, parentId);

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:68

  • The selector includes DISABLED certificate keys, although the backend resolver only accepts ACTIVE or PASSIVE keys, so choosing such an offered option is guaranteed to fail validation. Filter the metadata by enabled status before adding the option.
      if (k.certificate && k.providerId && !seen.has(k.providerId)) {

Copilot AI review requested due to automatic review settings August 6, 2026 09:45
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 6234bba to 510a41b Compare August 6, 2026 09:45

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 30 out of 30 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:68

  • The selector includes disabled certificate-bearing keys, but IdpClientCertificateResolver accepts only ACTIVE or PASSIVE keys. Selecting a disabled option therefore guarantees that saving the IdP fails; filter the metadata status to match the backend eligibility check.
      if (k.certificate && k.providerId && !seen.has(k.providerId)) {

@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 510a41b to 28b0d41 Compare August 6, 2026 11:54
Copilot AI review requested due to automatic review settings August 6, 2026 11:54
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 28b0d41 to 273148e Compare August 6, 2026 11:57

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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/base/src/test/java/org/keycloak/tests/model/ImportTest.java:215

  • This lookup still targets the realm attached to the run-on-server request, not the imported realm fetched above; IdentityProviderStorageProvider derives its realm from the session context, so the assertion will query the test framework's managed realm and return null. Set the session context to the imported realm before using session.identityProviders().
                IdentityProviderModel idp = session.identityProviders().getByAlias("mtls-oidc");

services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java:747

  • Each mTLS backchannel call constructs and closes a new pooled client; with the default max-connection-idle-time-millis, that also starts an eviction thread per token/userinfo request and prevents connection/TLS-session reuse. This creates substantial thread and handshake churn on the login path; reuse clients by IdP/key with lifecycle and rotation invalidation, or build a lightweight non-pooled per-call client without an evictor.
        return SimpleHttp.create(buildMtlsHttpClient(httpClientProvider), CLOSE_CLIENT)
                .withMaxConsumedResponseSize(httpClientProvider.getMaxConsumedResponseSize());

Copilot AI review requested due to automatic review settings August 6, 2026 12:00

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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (2)

services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java:747

  • Building and closing a full pooled client for every backchannel call defeats the configured global connection-pool limits and connection reuse; each concurrent login gets its own pool (and idle-evictor thread), so mTLS traffic is no longer capped by connection-pool-size/max-pooled-per-route and performs a fresh TLS handshake. Reuse clients keyed by IdP/key material with a managed invalidation/close lifecycle, or provide a dedicated design that preserves an aggregate connection limit.
        HttpClientProvider httpClientProvider = session.getProvider(HttpClientProvider.class);
        return SimpleHttp.create(buildMtlsHttpClient(httpClientProvider), CLOSE_CLIENT)
                .withMaxConsumedResponseSize(httpClientProvider.getMaxConsumedResponseSize());

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:20

  • This option is also rendered by AddOAuth2.tsx and the generic OAuth edit panel, but OAuth2IdentityProvider.fetchUserProfile still uses getUserInfoUrl() with SimpleHttp.create(session). A generic OAuth provider configured here can complete the mTLS token request and then fail while fetching the required profile; either route that call through the mTLS endpoint/client helpers or restrict this option to providers whose full backchannel path supports it.
  "tls_client_auth",

Copilot AI review requested due to automatic review settings August 6, 2026 23:24
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 273148e to 455b0a8 Compare August 6, 2026 23:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

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

Suppressed comments (3)

services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java:308

  • Replacing the existing validate(RealmModel) override breaks its public behavior: direct callers now reach IdentityProviderModel's no-op, and legacy subclasses that intentionally replaced validation by omitting super.validate(realm) are now forced through the generic checks (the Google-specific workaround demonstrates this). Keep the session-independent validation in the old override and have the new overload add only session-dependent mTLS validation via the base delegation, preserving existing SPI semantics.
    public void validate(KeycloakSession session, RealmModel realm) {

services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java:89

  • When a discovery document omits mtls_endpoint_aliases, these keys are absent from the returned map. The Admin UI merges imported keys with setValue (OpenIdConnectSettings.tsx:29-32), so importing a second document without aliases leaves the previous hidden mTLS URLs in place and requests keep going to stale endpoints. Ensure import explicitly clears all three alias fields when they are absent.
        MTLSEndpointAliases mtlsAliases = rep.getMtlsEndpointAliases();
        if (mtlsAliases != null) {

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:68

  • The selector includes DISABLED certificate keys, but IdpClientCertificateResolver rejects them (IdpClientCertificateResolver.java:24). This lets the UI offer a value that is guaranteed to fail on save; only include ACTIVE/PASSIVE keys.
      if (k.certificate && k.providerId && !seen.has(k.providerId)) {

Copilot AI review requested due to automatic review settings August 7, 2026 09:02
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 455b0a8 to 045e54d Compare August 7, 2026 09:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

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

Suppressed comments (1)

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:20

  • This option is also rendered by the generic OAuth 2 provider (AddOAuth2.tsx:79 and DetailSettings.tsx:538), but that provider is not fully mTLS-aware: OAuth2IdentityProvider.fetchUserProfile still uses SimpleHttp.create(session) and the regular user-info URL. A generic OAuth login can therefore complete the token exchange and then fail when its user-info endpoint requires the advertised client certificate; either scope this option to OIDC providers or route the OAuth provider's discovery/user-info path through the new mTLS client as well.
  "tls_client_auth",

Copilot AI review requested due to automatic review settings August 7, 2026 12:36
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from 045e54d to dd1765c Compare August 7, 2026 12:36

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 33 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (4)

services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java:287

  • When disable-trust-manager is configured, every per-request mTLS client rebuild executes this warning, turning each token/user-info/refresh/introspection call into a WARN entry. Keep the warning for initialization but avoid logging it for every dedicated client build.
        if (disableTrustManager) {
            logger.warn("TrustManager is disabled");
            builder.disableTrustManager();

services/src/main/java/org/keycloak/broker/oidc/mtls/IdpClientCertificateResolver.java:32

  • This accepts any non-null Key, but buildKeyManagers later requires a PrivateKey. A custom key provider returning another Key implementation therefore passes create/update validation and then deterministically fails every mTLS request; enforce the same PrivateKey predicate here.
        if (key.getPrivateKey() == null) {
            throw new IllegalStateException(
                    "Realm key " + keyProviderId + " has no private key; cannot be used for tls_client_auth.");

services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java:216

  • Every dedicated mTLS client runs configureBuilder, which reloads the configured global client-keystore from disk even though the subsequently supplied key managers take precedence and discard that keystore in resolveSslContext. This adds keystore I/O and parsing to every token, user-info, refresh, and introspection call; skip loading the global client keystore when custom TLS material is supplied, or cache the loaded store.
    CloseableHttpClient buildHttpClient(KeycloakSession session, SSLContext sslContext, KeyManager[] keyManagers) {
        HttpClientBuilder builder = newHttpClientBuilder(session);
        configureBuilder(builder, session);

services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java:275

  • configureBuilder now runs for every caller-owned mTLS client, so installations without a Keycloak truststore emit this warning once per backchannel request instead of once during shared-client initialization. Emit it once per factory/configuration (or suppress it for dedicated clients) to avoid log flooding during normal mTLS traffic.

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

        if (disableTruststoreProvider) {
            logger.warn("TruststoreProvider is disabled");

Signed-off-by: Sebastian Pfahl <eryx@gmx.net>
Copilot AI review requested due to automatic review settings August 10, 2026 05:43
@eryx12o45
eryx12o45 force-pushed the feature/38310-tls-client-auth-idp-brokering-fixed branch from dd1765c to a995aa4 Compare August 10, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

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

Suppressed comments (5)

services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java:287

  • This warning is also now reached for every dedicated mTLS backchannel client rather than only during shared-client initialization. With disable-trust-manager, normal broker traffic will repeatedly emit the same WARN; move it to one-time initialization or guard it so it is logged once.
        if (disableTrustManager) {
            logger.warn("TrustManager is disabled");
            builder.disableTrustManager();

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx:61

  • The dependency list omits realm, so after switching realms while this component remains mounted the selector keeps provider IDs from the previous realm and never refetches them. Include realm so the request and options follow the current realm.
    [clientAuthMethod],

server-spi-private/src/main/java/org/keycloak/http/simple/SimpleHttpRequest.java:258

  • This failure guard starts after request/URI/entity construction. If any of that setup throws (for example, an invalid URI), onCloseRequest() is never called and a CLOSE_CLIENT client leaks; wrap construction and execution in the close-on-failure guard, transferring ownership to SimpleHttpResponse only after execute succeeds.
        try {
            return new SimpleHttpResponse(client.execute(httpRequest), maxConsumedResponseSize, objectMapper, this::onCloseRequest);
        } catch (IOException | RuntimeException e) {
            onCloseRequest();
            throw e;

services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java:747

  • Every mTLS token, user-info, refresh, or introspection call builds a new pooling HTTP client. With the default idle-eviction setting, each build starts a cleaner thread, reloads configured TLS material, and prevents connection reuse, so broker traffic can create substantial thread and allocation churn; reuse a dedicated client for the session/IdP and close it at session completion, or build a non-pooling client without an evictor.
        HttpClientProvider httpClientProvider = session.getProvider(HttpClientProvider.class);
        return SimpleHttp.create(buildMtlsHttpClient(httpClientProvider), CLOSE_CLIENT)
                .withMaxConsumedResponseSize(httpClientProvider.getMaxConsumedResponseSize());

services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java:275

  • configureBuilder now runs for every dedicated mTLS client, so this warning—previously emitted once during lazy initialization—is logged on every backchannel request. A deployment using JVM-default trust will flood WARN logs under broker traffic; keep this diagnostic in the one-time initialization path or otherwise emit it once.

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

        if (disableTruststoreProvider) {
            logger.warn("TruststoreProvider is disabled");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support using tls_client_auth when brokering external IDP

2 participants