The Keycloak Upgrade Ledger

Every upgrade we have rehearsed, with the environment stated and the clock running.

2026-08-26 · run 2026-08-26-invalid-grant-401-to-400-narrowed

invalid_grant status change 401 → 400 — narrowed to 26.6.0

Scale
master realm only (auto-created), no seeded data
Database
Postgres 16, single node, fresh empty DB per version (docker compose down -v)
Topology
single container
Host
Hetzner ccx33 (8 vCPU / 32 GB), fsn1
Rehearsal
attempt 1 of 1 (diagnostic sweep, not a migration) — this lab holds a path to three clean runs, one exercising rollback

Environment

FieldValue
Keycloak versions26.0.0, 26.1.0, 26.2.0, 26.3.0, 26.4.0, 26.5.0, 26.5.7, 26.6.0, 26.6.4, 26.7.0, 26.7.1
Distributionquay.io/keycloak/keycloak (official image)
Start modestart w/ external DB
DatabasePostgres 16, single node, fresh empty DB per version (docker compose down -v)
Postgres tuningstock (compose db service sets none)
Adverse scenarionone — holds environment stock, varies the version
Dataset scalemaster realm only (auto-created), no seeded data
Topologysingle container
HostHetzner ccx33 (8 vCPU / 32 GB), fsn1
JVM heapcontainer default
Probeone password grant with a wrong password against admin-cli

Procedure

For each version, in order: purge volumes, set KC_VERSION, start db + keycloak, wait-ready.sh, then:

curl -s -w '\n%{http_code}' \
  -d "client_id=admin-cli" -d "username=admin" \
  -d "password=definitely-wrong" -d "grant_type=password" \
  "http://localhost:8080/realms/master/protocol/openid-connect/token"

A fresh DB per version guarantees a clean master realm, ruling out the "accumulated brute-force state" hypothesis left open in 2026-08-25-s7-custom-spi. Script: bin/narrow-invalid-grant.sh (one-off).

Result

VersionHTTP statusBody
26.0.0401{"error":"invalid_grant","error_description":"Invalid user credentials"}
26.1.0401same
26.2.0401same
26.3.0401same
26.4.0401same
26.5.0401same
26.5.7 (last 26.5 patch)401same
26.6.0400same
26.6.4400same
26.7.0400same
26.7.1400same

The change landed in 26.6.0. The entire 26.5 line, including its final patch 26.5.7, returns 401; 26.6.0 onward returns 400. Body text is unchanged.

Verification

ClaimPrimary sourceChecked
Change is version-caused, not realm statefresh DB per version, clean master realm
Boundary is 26.6.0, not a 26.5.x patch26.5.7 (last 26.5 patch) = 401, 26.6.0 = 400
Entire 26.6+ line is 40026.6.0, 26.6.4, 26.7.0, 26.7.1 all 400
RFC 6749 says 400 for invalid_grantRFC 6749 §5.2 (as noted in s9-ldap-federation.md)
Whether the change is in Keycloak release noteschecked 2026-08-26 — issue #45812 "ROPC: invalid_grant Error Response not RFC Compliant" (area oidc) in the 26.6.0 release notes

Publishable extract

A failed password grant against a Keycloak token endpoint answers 401 on 26.0.0 through 26.5.7, and 400 from 26.6.0 onward — same request, same wrong password, identical Invalid user credentials body. The change landed in 26.6.0. RFC 6749 specifies 400 for invalid_grant, so the newer behaviour is the correct one; but any client keying on 401 to detect a bad password breaks on upgrade past 26.6.0, and that code is common.

← Back to the Ledger