# Patch Analysis — CVE-2026-11800 (Keycloak JWT Algorithm Confusion)

Fix reference: Keycloak PR #50374, commit `d343c7373dcc7bfeb9dc14de2309836299c81837`
("Remove support for symmetric algorithms in JWT public key validators"), closes
GitHub issue #50357. Released in Keycloak 26.6.4 / 26.7.0.

## 1. What the fix changes (files, functions, logic)

The fix has two coordinated layers:

### Layer 1 — Algorithm-type gate in `AbstractBaseJWTValidator.validateSignatureAlgorithm()`
File: `services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractBaseJWTValidator.java`

- Adds `protected boolean isSymmetricAlgorithmAllowed() { return false; }` (default deny
  for symmetric algorithms).
- Rewrites `validateSignatureAlgorithm(String expectedSignatureAlg)` to:
  1. reject a `null` algorithm (pre-existing);
  2. **reject `"none"` explicitly**;
  3. **when `!isSymmetricAlgorithmAllowed()`, resolve the
     `ClientSignatureVerifierProvider` for the JWT's algorithm and reject unless
     `signatureProvider.isAsymmetricAlgorithm()` is true** — i.e. HS256/HS384/HS512 are
     rejected for every validator that does not opt in;
  4. then enforce `expectedSignatureAlg` if configured (pre-existing).
- Adds `JWTClientSecretValidator extends JWTClientValidator` overriding
  `isSymmetricAlgorithmAllowed()` to `true`, and changes
  `JWTClientSecretAuthenticator` to use it — so legitimate `client_secret_jwt` (HMAC with
  the real client secret) still works.

This gate is invoked by **every `AbstractBaseJWTValidator` subclass** via
`AbstractJWTClientValidator.validate()` (the ordered chain:
`validateClientAssertionParameters → validateClient → validateSignatureAlgorithm →
validateSignature → validateTokenAudience → validateTokenActive`) and by
`DefaultJWTAuthorizationGrantValidator` (JWT Authorization Grant) which extends
`AbstractBaseJWTValidator` directly.

### Layer 2 — Remove the HMAC key-loading branch
File: `services/src/main/java/org/keycloak/keys/loader/HardcodedPublicKeyLoader.java`
+ `core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java`

- Removes the `else if (JavaAlgorithm.isHMACJavaAlgorithm(algorithm)) { keyWrapper.setType(KeyType.OCT); keyWrapper.setSecretKey(KeyUtils.loadSecretKey(Base64Url.decode(encodedKey), algorithm)); }` branch from `HardcodedPublicKeyLoader`.
- Replaces it with `else { logger.warnf("Unrecognized or invalid algorithm %s for hardcoded public key", algorithm); kw = null; }`.
- Removes `JavaAlgorithm.isHMACJavaAlgorithm(...)` entirely, so no remaining code can
  load an HMAC secret via that helper (compile-time guarantee).

### Behavior before vs after
| Config | Vulnerable (≤26.6.3) | Fixed (≥26.6.4) |
|---|---|---|
| JWT Authorization Grant, `useJwksUrl=false`, `publicKeySignatureVerifier`=base64 RSA pubkey, assertion `alg=HS256` | ACCEPTS (Layer 1 has no symmetric check; Layer 2 HMAC branch turns the public key DER bytes into the HMAC secret) | REJECTS `Invalid signature algorithm` (Layer 1) and key load fails (Layer 2) |
| Same, `alg=none` | Layer 1 did not reject `none`; signature provider lookup fails | REJECTS `Invalid signature algorithm` (explicit `none` rejection) |

## 2. Assumptions the fix makes

- **A1**: Every attacker-controlled JWT that is verified against an Identity Provider's
  hardcoded public key passes through `AbstractBaseJWTValidator.validateSignatureAlgorithm()`
  (Layer 1) **before** signature verification.
- **A2**: The only key loader that could turn a hardcoded public-key config into an HMAC
  secret is `HardcodedPublicKeyLoader`, and removing `isHMACJavaAlgorithm()` eliminates all
  such branches.
- **A3**: Symmetric algorithms are only legitimate for `client_secret_jwt`
  (`JWTClientSecretValidator`), where the HMAC key is the real client secret (not a public
  key), so algorithm confusion does not apply there.

## 3. Code paths / inputs the fix does NOT cover (the gap this variant exploits)

`AbstractBaseJWTValidator.validateSignatureAlgorithm()` (Layer 1) is **only** on the client
authentication / JWT Authorization Grant validator chain. **OIDC Identity Provider
token-introspection-like flows that verify an external JWT directly do NOT call it.**

Specifically, the **OIDC external-internal token exchange** path
(RFC 8693, `grant_type=urn:ietf:params:oauth:grant-type:token-exchange` with
`subject_token_type=urn:ietf:params:oauth:token-type:jwt` and `subject_issuer=<oidc-idp>`):

`TokenExchangeGrantType` → `V1TokenExchangeProvider.tokenExchange()` →
`exchangeExternalToken()` → `OIDCIdentityProvider.exchangeExternal()` →
`exchangeExternalImpl()` → `validateJwt()` → `validateToken()` → `verify()` →
`verifySignature(JWSInput)` → `PublicKeyStorageManager.getIdentityProviderKeyWrapper()` →
`HardcodedPublicKeyLoader(kid, encodedKey, alg)`.

`OIDCIdentityProvider.verifySignature()` (services/.../broker/oidc/OIDCIdentityProvider.java,
~line 701) resolves the `SignatureProvider` for the JWT header's `alg` and calls
`signatureProvider.verifier(publicKey).verify(...)` **without any algorithm-type
allow-list check**. There is no call to `AbstractBaseJWTValidator.validateSignatureAlgorithm()`
on this path.

**Consequence:** On the fixed version, this path is protected **only by Layer 2** (the
`HardcodedPublicKeyLoader` HMAC-branch removal). Layer 1 does not gate it. If a future
change reintroduced an HMAC-capable hardcoded-key loader (or any key loader that derives an
HMAC secret from a public-key config), the external token-exchange path would be re-exposed
to HS256 algorithm confusion with **no algorithm-type gate** to catch it.

Other Layer-1-uncovered, Layer-2-covered paths that share the same `verifySignature` sink:
- OIDC broker ID-token / logout-token validation (`OIDCIdentityProvider.validateToken` /
  `verify`) when the brokered IdP uses a hardcoded public key — but the ID token arrives
  server-to-server from the upstream IdP (not directly attacker-controlled), so it is not a
  practical variant (see RCA report).
- `DefaultTrustIdentityProvider` (trust-material broker) uses `HardcodedPublicKeyLoader`
  with a request-supplied algorithm — also Layer-2-only.

## 4. Is the fix complete?

For the **JWT Authorization Grant** (the disclosed CVE surface): **yes** — Layer 1 rejects
HS256 before signature verification, and Layer 2 removes the HMAC branch; defense in depth.

For the **OIDC external token exchange** (this variant): **covered, but only by Layer 2**.
The fix closes the immediate exploit (the HMAC branch is gone, so `HardcodedPublicKeyLoader`
returns `null` for `HS256` and `verifySignature` fails → `invalid_token`), but the fix's
primary, explicit algorithm-type gate (Layer 1) does **not** run on this path. The fix is
therefore **not structurally complete** for this entry point: it relies on a single layer
(the key-loader change) rather than the two-layer defense used for the JWT Authorization
Grant.

## 5. Target threat-model scope (SECURITY.md / policy)

Keycloak's stated scope (GitHub issue #50357, GHSA-gqj5-2xp5-3qmp, Red Hat advisory) is
JWT algorithm confusion in the JWT Authorization Grant flow: "an attacker with valid client
credentials can bypass signature verification by forging an assertion ... impersonate any
federated user linked to the affected Identity Provider." The external token-exchange
variant is the same vulnerability class (JWT algorithm confusion against a hardcoded IdP
public key, attacker-controlled JWT over the network API, federated-user impersonation) and
the same trust boundary (unauthenticated/low-privilege network caller submitting a forged
JWT to the token endpoint). It is in-scope. The variant requires a legitimately configured
OIDC IdP (hardcoded public key, `validateSignature=true`) and a client authorized for
token exchange to that IdP — the same admin-configured prerequisites as the original CVE
(`oauth2.jwt.authorization.grant.enabled` + `oauth2.jwt.authorization.grant.idp`).

## 6. Recommendation to close the gap

Add an explicit algorithm-type allow-list check to `OIDCIdentityProvider.verifySignature()`
(and the shared broker `verify` path) mirroring `AbstractBaseJWTValidator.validateSignatureAlgorithm()`:
reject `none`, and reject symmetric algorithms (HS256/HS384/HS512) unless the IdP is
explicitly configured to allow symmetric client-assertion verification. This makes the
external token-exchange (and broker ID-token) path two-layer-defended like the JWT
Authorization Grant, so the fix no longer depends solely on `HardcodedPublicKeyLoader` not
reintroducing an HMAC branch.
