CVE-2026-97846: Keycloak Token Exchange Drops mTLS Binding

Guilliano Molaire Guilliano Molaire 11 min read

Last updated: September 2026

CVE-2026-97846 is a Keycloak flaw where standard token exchange (the “V2” token exchange that became fully supported in Keycloak 26.2) does not enforce mTLS holder-of-key binding. A client configured to receive only certificate-bound access tokens can still get an ordinary Bearer token through the exchange endpoint, without presenting its certificate. Red Hat published it on 25 September 2026, rated it Moderate with a CVSS 3.1 base score of 6.8, and no fixed Keycloak release exists yet.

The practical meaning is that anyone who steals that client’s credentials, such as a client secret, and holds a valid subject token can mint a token that works without the certificate. That is exactly the scenario certificate binding exists to prevent. If you have not turned on both standard token exchange and certificate-bound tokens for the same client, this one does not reach you.

What does CVE-2026-97846 break?

It breaks the promise behind the client setting OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled. In the CVE record Red Hat published on 25 September 2026, the flaw is described as the new Standard Token Exchange V2 feature not checking for the client certificate, which “allows an attacker with stolen client credentials to obtain a standard, unrestricted token that bypasses these security protections” (CVE Program, CVE-2026-97846 record, 2026).

mTLS holder-of-key tokens versus ordinary Bearer tokens

A Bearer token works for whoever holds it, so anyone who obtains a copy can use it. RFC 8705, the IETF’s 2020 standard for OAuth 2.0 mutual TLS, defines certificate-bound access tokens as the fix: the authorization server puts a cnf (confirmation) claim containing x5t#S256, the SHA-256 thumbprint of the client’s TLS certificate, into the token. A resource server that honours it only accepts the token over a TLS connection authenticated with that same certificate. Stealing the token is then not enough, because the attacker also needs the certificate’s private key.

A token without cnf is a plain Bearer token again, and that is what the exchange endpoint can hand back here.

Where the certificate check goes missing in the source

The mechanism is visible in the Keycloak source on GitHub, and it is a missing call rather than a wrong one. For most grant types, Keycloak builds the token response in OAuth2GrantTypeBase, which calls checkAndBindMtlsHoKToken whenever the client has certificate-bound tokens enabled. If no client certificate arrived with the request, that method stops the request:

if (clientConfig.isUseMtlsHokToken()) {
    AccessToken.Confirmation confirmation = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request, session);
    if (confirmation != null) {
        responseBuilder.getAccessToken().setConfirmation(confirmation);
        ...
    } else {
        String errorMessage = "Client Certification missing for MTLS HoK Token Binding";

Standard token exchange does not go through that response builder. StandardTokenExchangeProvider assembles its own response with tokenManager.responseBuilder(...), and checkAndBindMtlsHoKToken is never called on that path.

What happens next depends on your minor line, based on our reading of the release tags. In the 26.4 and 26.6 source, the standard exchange path contains no certificate binding at all, so the exchanged access token carries no cnf claim whether or not a certificate was presented. In the 26.7 source, a transient mtls-hok protocol mapper adds the cnf claim when a certificate happens to be present on the request, but nothing rejects a request that arrives without one. On every line we read, nothing in the exchange path rejects a request that has valid client credentials and an unbound subject token but no certificate, so such a request would come back with an unbound token. We are describing the code rather than a test we ran, which is why the audit section below shows how to confirm it on your own build.

The client policy executor you might expect to catch this does not. holder-of-key-enforcer, the executor used by FAPI profiles, checks for a certificate on authorization code, service account and CIBA token requests, on refresh, revocation, userinfo and logout. In the current source its switch statement has no case for token exchange requests.

Which Keycloak deployments are exposed?

You are exposed only if one client has both standard token exchange and certificate-bound access tokens enabled. Red Hat scores the attack complexity as High and the privileges required as Low (vector CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N, CWE-287 Improper Authentication), which matches a real but narrow set of preconditions.

The client has the Standard Token Exchange switch turned on. It is off by default: OIDCAdvancedConfigWrapper.isStandardTokenExchangeEnabled() reads the standard.token.exchange.enabled attribute with a default of false. If you never enabled it on a certificate-bound client, the exchange endpoint rejects that client before any of this matters. Legacy token exchange (V1, a preview feature) is a separate provider and is not what this CVE names.

The client has certificate-bound tokens enabled. This is the tls.client.certificate.bound.access.tokens attribute, shown in the admin console under the client’s Advanced tab. It is mostly seen in FAPI and open banking deployments.

The client authenticates with something other than its certificate. This is the condition people miss. If the client authenticates at the token endpoint with a client secret or with private_key_jwt, an attacker who steals that secret or key can call the exchange endpoint without the TLS certificate. If the client authenticates with tls_client_auth (the X.509 client authenticator), there is no separate credential to steal, and anyone able to authenticate as that client already holds the certificate.

An attacker also holds a usable subject token. Standard token exchange needs an access token for the user whose identity is being exchanged, and the requesting client must be in that token’s audience or be the client it was issued to. That subject token must itself be an unbound Bearer token, typically one issued to another client that lists this client in its audience. A certificate-bound subject token does not work for the attacker: 26.6 rejects sender-constrained subject tokens outright, and 26.7 only accepts one issued to the requesting client and then verifies it against the presented certificate.

If you want a refresher on how the exchange grant itself works before auditing clients, our Keycloak token exchange practical guide walks through the request and the client settings. For service-to-service setups in general, Keycloak machine-to-machine authentication covers the client credentials side.

How do I check whether a client is affected?

List clients that have both attributes set. With the Admin REST API and jq, this is one call per realm:

curl -s -H "Authorization: Bearer $ADMIN_TOKEN" 
  "https://auth.example.com/admin/realms/myrealm/clients?max=500" 
  | jq -r '.[] | select(.attributes["standard.token.exchange.enabled"] == "true"
            and .attributes["tls.client.certificate.bound.access.tokens"] == "true")
          | "(.clientId)t(.clientAuthenticatorType)"'

Any client this prints is in scope. The second column tells you how it authenticates: client-secret, client-secret-jwt or client-jwt means a stolen secret or signing key is enough to reach the exchange endpoint, and client-x509 means the certificate is itself the credential.

To confirm the behaviour on your own build, perform a standard token exchange for one of those clients from a machine that does not present the client certificate, then decode the returned access token and look for cnf. Our JWT token analyzer decodes the claims in the browser if you would rather not pipe a live token through a shell. A token without cnf from a certificate-bound client is the bug.

What should I do before a fix ships?

Turn off standard token exchange on certificate-bound clients unless you need both, and move the ones that need both to certificate-based client authentication. As of 25 September 2026, the CVE record lists no fixed version, Red Hat states that no mitigation meets its own criteria, and the newest community releases on each maintained line (26.7.4, 26.6.7 and 26.4.16) predate the disclosure.

Disable Standard Token Exchange where it is not required. On each client from the audit above, open Clients > your client > Settings and switch off Standard Token Exchange. Standard exchange then declines that client, which removes the precondition rather than softening the result. One caveat: if the server also runs with the legacy token-exchange preview feature enabled, the V1 provider can pick up requests that standard exchange declines, subject to its own fine-grained permission checks, and in our reading it does not require a certificate either. Check your startup --features flags as part of the same audit.

Switch the remaining clients to tls_client_auth. If a client genuinely needs both features, change its Client Authenticator to X509 Certificate under the Credentials tab. A stolen secret stops being useful because there is no secret, and on 26.7 the transient mapper will then bind exchanged tokens to the certificate that authenticated the call.

Make resource servers require cnf rather than merely honour it. RFC 8705 tells a resource server to verify the binding when cnf is present. An API that serves only certificate-bound clients should also reject tokens that lack cnf, and that single check makes an unbound exchanged token useless against it. We cover what else strict validation should include in how to verify a Keycloak-issued access token on the backend.

Rotate secrets that may have leaked. The attack starts with stolen client credentials. If a certificate-bound client’s secret has ever been in a place you would not want it, rotate it now, because that is the part an attacker cannot do without.

Watch token exchange events. Keycloak records a TOKEN_EXCHANGE event for each exchange. Exchange calls from a certificate-bound client that come from a network location where that client does not normally run are worth an alert while the fix is outstanding.

Keycloak does ship security fixes on older minor lines, not only the newest one. Both 26.4.15 and 26.6.6 landed on 11 August 2026, after 26.7.0 was already out, so check your own line’s release notes for this CVE rather than assuming you must jump to 26.7. If you run the Red Hat build of Keycloak, follow Red Hat’s advisory for the product version that carries the fix. We keep upgrade sequencing advice in our Keycloak cluster upgrade strategy.

Why does sender-constraint matter for token exchange in particular?

Token exchange is the grant that turns one token into another, so any binding that is not re-applied during exchange disappears at the first hop. In September 2026, NIST finalized Interagency Report 8587, “Protecting Tokens and Assertions from Forgery, Theft, and Misuse,” and its announcement names sender-constrained tokens, with mutual TLS specifically, among the recommended controls, and we covered how that maps onto Keycloak in NIST IR 8587 token protection with Keycloak.

The lesson we take from reading this code path is structural. Keycloak’s binding logic for mTLS lives in the shared token response builder for grant types, and a provider that assembles its own response has to remember to call it. Standard token exchange did not, and the FAPI enforcer’s list of events did not include exchange either, so both the shared grant-type response builder and the FAPI executor missed the exchange path. When you evaluate a new grant or extension in any identity provider, it is worth asking specifically whether it re-applies sender constraints, because it is an easy step to leave out.

DPoP, the application-level alternative defined in RFC 9449 (2023), is not what this CVE names. In the current source, standard token exchange does validate a DPoP proof when the subject token is DPoP-bound, and the transient DPoP mapper runs for OpenID Connect clients. We have not tested exchange-issued DPoP binding end to end, so treat that as a reading of the code rather than a guarantee. For DPoP setup, see DPoP with the Keycloak Admin API using Node.js.

What does managed Keycloak change here?

Managed hosting changes who applies the fix and how quickly, not whether your client design is sound. On Skycloak, identity management as a service built on upstream Keycloak, we roll security releases onto customer clusters, so the code fix arrives without you scheduling an upgrade. Which clients enable token exchange, how they authenticate, and whether your APIs insist on cnf remain your configuration choices, and the audit above applies to a managed realm exactly as it does to a self-hosted one.

If you are working through a broader review, our Keycloak security audit and hardening checklist covers client authentication and token settings alongside everything else.

Frequently asked questions

Is CVE-2026-97846 exploitable without stolen credentials?

No. The CVE record describes an attacker with stolen client credentials, and Red Hat’s CVSS vector sets privileges required to Low. The attacker must authenticate as the certificate-bound client and present a valid subject token. The flaw removes the certificate requirement, not the need to authenticate as the client.

Which Keycloak versions are affected by CVE-2026-97846?

The CVE record published on 25 September 2026 lists the Red Hat build of Keycloak as affected without version ranges, and Red Hat Single Sign-On 7 as unaffected. Standard token exchange became fully supported in Keycloak 26.2, and every standard exchange provider we read from 26.4 through 26.7.4 lacks the certificate requirement. Test your own build.

Does disabling token exchange fully mitigate it?

Yes, for that client. With the Standard Token Exchange switch off, the standard exchange provider reports that it does not support the request, so it issues nothing for that client. The exception is a server that also enables the legacy token-exchange preview feature, where the V1 provider may handle the request instead, so confirm that flag is off. The switch defaults to off, so realms that never enabled it on a certificate-bound client are not exposed.

Does the FAPI holder-of-key-enforcer policy block this?

Not in the current source. The holder-of-key-enforcer client policy executor requires a certificate on authorization code, service account and CIBA token requests, and checks refresh, revocation, userinfo and logout. Its event list has no token exchange case, so a FAPI client profile alone does not close this gap.

Is this the same as CVE-2026-93999?

No. CVE-2026-93999, published on 19 September 2026, is also a standard token exchange flaw, but it concerns a disabled audience client being restored on refresh. CVE-2026-97846 is about the certificate binding. We covered the earlier one in CVE-2026-93999: refresh restores a disabled audience.

Sources

  • CVE Program, “CVE-2026-97846: standard token exchange v2 bypasses mtls holder-of-key binding”, CNA record from Red Hat, published 25 September 2026, retrieved 2026-09-25, https://www.cve.org/CVERecord?id=CVE-2026-97846 (record JSON: https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/97xxx/CVE-2026-97846.json)
  • Red Hat Customer Portal, “CVE-2026-97846”, https://access.redhat.com/security/cve/CVE-2026-97846 (linked from the CVE record; not reachable from our research environment)
  • Red Hat Bugzilla, bug 2540949, https://bugzilla.redhat.com/show_bug.cgi?id=2540949
  • Keycloak, “Standard Token Exchange is now officially supported in Keycloak 26.2”, keycloak.org blog, 2025, https://www.keycloak.org/2025/05/standard-token-exchange-kc-26-2 (not reachable from our research environment; the 26.2.0 tag in keycloak/keycloak on GitHub, dated 11 April 2025, contains the provider)
  • IETF, “RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens”, 2020, retrieved 2026-09-25, https://datatracker.ietf.org/doc/html/rfc8705
  • Keycloak, StandardTokenExchangeProvider.java, keycloak/keycloak on GitHub, retrieved 2026-09-25, https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java
  • Keycloak, OAuth2GrantTypeBase.java, keycloak/keycloak on GitHub, retrieved 2026-09-25, https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java
  • Keycloak, HolderOfKeyEnforcerExecutor.java, keycloak/keycloak on GitHub, retrieved 2026-09-25, https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforcerExecutor.java
  • Keycloak releases, tags 26.7.4, 26.6.7 and 26.4.16, retrieved 2026-09-25, https://github.com/keycloak/keycloak/releases

Patched on the day, not on your next maintenance window

Keycloak 26.7.2 fixed CVE-2026-18963, an account takeover through password reset. Skycloak had it available for auto-upgrade and told customers the same day upstream shipped it. Self-hosted teams schedule that work themselves.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman