CVE-2026-93999: Keycloak Refresh Restores a Disabled Audience

Guilliano Molaire Guilliano Molaire 9 min read

Last updated: September 2026

Keycloak checks whether an audience client is enabled when you perform a standard token exchange, and does not check again when the refresh token from that exchange is used. CVE-2026-93999, published on 19 September 2026, is that missing second check. If a client was disabled after the exchange happened, refreshing still produces an access token carrying the disabled client in aud, because the audience is restored from a claim stored in the refresh token rather than re-resolved against current state.

Red Hat rates it CVSS 3.1 base 4.2, vector CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N, classified CWE-862 Missing Authorization (GitHub Security Advisory GHSA-82wc-7jr6-wxgm, “A flaw was found in the OIDC protocol implementation of Keycloak”, 2026). The preconditions are narrow enough that most realms are not affected, and specific enough that you can check yours in one look at a client setting.

What does CVE-2026-93999 actually allow?

A client holding a refresh token that was issued by a standard token exchange can keep obtaining access tokens naming an audience client that an administrator has since disabled. The advisory describes a failure to validate that the target audience client is still active before issuing, which “permits applications holding existing refresh tokens to obtain valid access tokens for disabled clients, potentially circumventing access controls in offline JWT validation scenarios.”

The mechanism is visible in the Keycloak source, and it is more specific than the advisory text suggests. When you call standard token exchange with an audience parameter, StandardTokenExchangeProvider resolves the target clients and rejects any that are switched off:

if (!targetClient.isEnabled()) {
    event.detail(Details.REASON, "audience client disabled");

Having passed that check, the resolved clients are stashed on the session context as REQUESTED_AUDIENCE_CLIENTS, declared in Constants.java as req-aud-clients with the comment “attribute name used to set clients from requested audience in standard token exchange.” If that exchange also mints a refresh token, their client IDs are written into it as the req-aud claim, commented in the same file as “claim used in refresh token to know the requested audience.”

On the next refresh, AbstractRefreshTokenProvider reads that claim back and turns the IDs into clients again:

.map(clientId -> session.clients().getClientByClientId(realm, clientId))
.filter(Objects::nonNull)

A client that no longer exists is dropped by that nonNull filter. A client that exists but is disabled is not. That asymmetry is the vulnerability: deletion is honoured on refresh, disabling is not.

Which Keycloak configurations are actually exposed?

Three conditions have to hold together, which is why the attack complexity is rated High.

You use standard token exchange with an audience parameter. The req-aud claim is written only on that path. A client that never performs a token exchange never gets one.

That exchange issues refresh tokens. Keycloak only returns a refresh token from an exchange when requested_token_type is the refresh token type, the client has refresh tokens enabled, and the client’s standard token exchange refresh setting is not NO:

if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)
    && oidcClient.isUseRefreshToken()
    && oidcClient.getStandardTokenExchangeRefreshEnabled() !=
       OIDCAdvancedConfigWrapper.TokenExchangeRefreshTokenEnabled.NO) {

That setting is the one to look at first, because it is the cheapest thing to turn off.

A resource server validates the token offline. An API that checks signature, iss, exp and aud against cached JWKS is the one that cannot observe an administrative change. That is the normal, recommended pattern, which is what makes this worth knowing about.

If you want background on how the exchange itself works before auditing your clients, we cover it in the Keycloak token exchange practical guide.

This is not the same thing as a hardcoded audience mapper

Worth separating, because the two look identical from outside and only one is a CVE. An Audience protocol mapper adds a configured string to aud on every issuance. It performs no client lookup and never checked whether that client was enabled, on any version, before or after this fix. Disabling a client will not remove it from a hardcoded mapper’s output, and that behaviour is not CVE-2026-93999. If your aud comes from a mapper, you are looking at configuration, not this vulnerability.

How do I reproduce CVE-2026-93999?

The reproduction has to go through token exchange. A mapper-based test will show a disabled client in aud on every Keycloak build ever released and tell you nothing.

  1. On client api-a, enable standard token exchange and set Token exchange refresh token enabled to a value other than No in the client’s advanced settings.
  2. Obtain a subject token, then exchange it requesting audience=api-b and requested_token_type=urn:ietf:params:oauth:token-type:refresh_token. Keep the refresh token.
  3. In the admin console, open Clients > api-b > Settings and toggle Enabled off. Save.
  4. Confirm a fresh exchange now fails, which is the check that does work.
  5. Refresh using the token from step 2 and read aud on the result.
curl -s -X POST 
  -d "grant_type=refresh_token" 
  -d "client_id=api-a" 
  -d "client_secret=$SECRET" 
  -d "refresh_token=$EXCHANGED_REFRESH_TOKEN" 
  "https://auth.example.com/realms/myrealm/protocol/openid-connect/token" 
  | jq -r .access_token | cut -d. -f2 | base64 -d 2>/dev/null | jq .aud

If step 4 fails and step 5 still shows api-b, that gap between the two is the bug. Our JWT token analyzer will decode the claim set in the browser when you would rather not pipe a live token through a shell.

What should I do about it now?

Turn off token exchange refresh tokens on the affected clients, and revoke sessions when you disable a client instead of treating the toggle as a revocation.

The advisory as published on 19 September 2026 names no fixed version, and the most recent stable release at the time of writing is 26.7.4, published 16 September 2026, which predates it. These are the current answers rather than a stopgap for a release you can install today.

Set token exchange refresh tokens to No where you do not need them. No req-aud claim is written without a refresh token from an exchange, so this removes the precondition rather than mitigating the consequence. Most exchange use cases want a short-lived access token and nothing else.

Revoke sessions as part of the disable runbook. Disabling a client leaves existing sessions untouched by design. POST /admin/realms/{realm}/logout-all is the blunt instrument. The not-before route is two steps, and conflating them is common: you first set the realm’s not-before value, in Realm settings > Sessions > Revocation, which invalidates tokens issued before that moment, and then push it to clients that expose an admin URL:

curl -X POST 
  -H "Authorization: Bearer $ADMIN_TOKEN" 
  "https://auth.example.com/admin/realms/myrealm/push-revocation"

If you rely on not-before, Keycloak 26.7.3 fixed CVE-2026-18218, where a client not-before was ignored when the realm value was older but nonzero. Check your line carries that fix first. We wrote that release up in the Keycloak 26.7.3 patch checklist.

Shorten access token lifespans on clients that perform exchanges. The stale audience persists only as long as a minted access token stays valid, so a five minute lifespan caps the exposure at five minutes.

Rotate refresh tokens with reuse detection. Rotation limits how long any single captured refresh token remains useful and gives you a signal when an old one reappears. The setup is in our Keycloak refresh token rotation guide.

Why can’t the resource server just detect this?

Because nothing on the validation path checks whether an audience client is enabled, and that includes introspection. Offline JWT validation is arithmetic over a signed blob, so it obviously cannot. Introspection is the interesting case, since it is the usual answer to “I need current state.”

Keycloak’s introspection endpoint verifies that the client the token was issued for is enabled, and that the introspecting client appears in the token’s audience. Neither of those asks whether the other clients named in aud are still enabled. A third client introspecting a token from the scenario above gets active: true.

The practical conclusion is a posture change rather than a configuration one. Disabling a client is an administrative state change, not a revocation primitive, and no token validation mechanism treats it as one. Revocation is what revokes: sessions, not-before, and short lifetimes. We work through those tradeoffs in JWT token lifecycle management.

What belongs in the hardening pass after this one?

Audience hygiene is the durable fix, and most realms carry more audience than they need.

Control Why it matters here
Audit which clients enable standard token exchange The precondition for the whole issue
Set token exchange refresh tokens to No by default Removes the req-aud claim entirely
Validate aud strictly at every resource server A token for another audience should be rejected outright
Revoke sessions when disabling a client Makes the admin action do what operators assume
Track fixes on your own minor line Point releases land on older minors, not only the newest

On that last row: Keycloak does ship fixes on older minor lines. Both 26.4.15 and 26.6.6 landed on 11 August 2026, after 26.7.0 was already out. Whether your line carries a fix is a different question from whether the newest release does.

If your resource servers validate aud loosely, that is the first thing to change, and our JWT best practices post covers what strict validation looks like. For the adjacent case where an MCP server rejects a token because the audience never matched to begin with, see the RFC 8707 audience post, which is a configuration problem rather than this bug.

Frequently asked questions

Does disabling a client in Keycloak revoke its tokens?

No. Disabling a client stops new authentication through it, and the client’s own credentials stop working at the token endpoint, but tokens already issued remain cryptographically valid until they expire or a revocation invalidates them. CVE-2026-93999 extends that further than expected, to audiences restored on refresh.

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

The advisory published on 19 September 2026 specifies no affected or patched version ranges. The most recent stable release at the time of writing was 26.7.4, from 16 September 2026, which predates publication. Reproduce it against your own build rather than inferring from a version number.

Am I affected if I only use audience protocol mappers?

Not by this CVE. A hardcoded audience mapper adds a configured string to aud with no client lookup, so it never consulted enabled state on any version. That is expected mapper behaviour, though it is still worth auditing, since it means a disabled client can appear in aud for unrelated reasons.

Does token introspection detect a disabled audience client?

No. Keycloak’s introspection endpoint checks that the client the token was issued for is enabled and that the introspecting client is in the audience. It does not evaluate whether other clients named in aud are still enabled, so introspection returns active for these tokens.

What is the single fastest mitigation?

Set Token exchange refresh token enabled to No on clients that perform standard token exchange and do not need refresh tokens from it. Without a refresh token from an exchange, the req-aud claim is never written, and the vulnerable restore path is never reached.

Sources

  • GitHub Security Advisory, “GHSA-82wc-7jr6-wxgm: A flaw was found in the OIDC protocol implementation of Keycloak”, published 19 September 2026, retrieved 2026-09-20, https://github.com/advisories/GHSA-82wc-7jr6-wxgm
  • NIST National Vulnerability Database, “CVE-2026-93999”, retrieved 2026-09-20, https://nvd.nist.gov/vuln/detail/CVE-2026-93999
  • Red Hat Customer Portal, “CVE-2026-93999”, retrieved 2026-09-20, https://access.redhat.com/security/cve/CVE-2026-93999
  • Keycloak, Constants.java, REQUESTED_AUDIENCE_CLIENTS and REQUESTED_AUDIENCE, keycloak/keycloak on GitHub, retrieved 2026-09-20, https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/models/Constants.java
  • Keycloak, AbstractRefreshTokenProvider.java, keycloak/keycloak on GitHub, retrieved 2026-09-20, https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java
  • Keycloak, StandardTokenExchangeProvider.java, keycloak/keycloak on GitHub, retrieved 2026-09-20, https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java
  • Keycloak releases, tag 26.7.4 published 16 September 2026, retrieved 2026-09-20, 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