v26.3.10
v26.3.10
Dynamic client registration omits optional client metadata
The OpenID Connect dynamic client registration endpoints under /oauth2/register no longer return client_uri, logo_uri,
policy_uri, tos_uri, contacts, grant_types, response_types, and jwks when those fields have no value. RFC 7591 marks
them optional, and responses previously included them as an empty string, null, or an empty object.
This unblocks clients that check the registration response against a strict schema, where such a field is accepted only when it's
absent or correctly typed: an empty string isn't a valid URI, null isn't a valid array, and an empty object isn't a valid JSON
Web Key Set. Model Context Protocol (MCP) clients that register themselves through dynamic client registration failed on this.
Send a value for a field when you register or update a client to have it appear in the response.
The registration and update responses also no longer return metadata, matching the read response. metadata can't be set
through dynamic client registration, so sending the field back to PUT /oauth2/register/{id} — as RFC 7592 has a client do with
the values it received — was rejected with invalid_client_metadata.
The admin endpoints under /admin/clients, and the client object embedded in login, consent, and device request payloads, are
unchanged and still return these fields as empty-string, null, or an empty object.
Upgrading from Hydra 1.0.0-rc or earlier needs a stepped upgrade
hydra migrate sql up no longer converts the pre-1.0.0 migration tables (hydra_client_migration, hydra_jwk_migration,
hydra_oauth2_authentication_consent_migration, and hydra_oauth2_migration) into the schema_migration table.
This only affects a database whose last migration ran on Hydra 1.0.0-rc or earlier, meaning before June 2019. Every Hydra release
since 1.0.0 performed the conversion automatically on the first hydra migrate sql up and then dropped the old tables. If you
have run hydra migrate sql up at any point since June 2019, you are unaffected and need to do nothing.
Breaking changes
If your database still has the tables listed above, upgrading straight to this release fails during migration with an error such
as relation "hydra_client" already exists, because Hydra no longer recognizes the old tables and tries to apply the 2019
migrations from the beginning. No data is lost, but the upgrade does not complete.
To upgrade, back up your database, then run hydra migrate sql up once with any Hydra release from 1.0.0 (June 2019) up to the
previous release to convert the migration tables. Then upgrade to this release and run hydra migrate sql up again.
To check whether you are affected, confirm the old tables are gone:
SELECT table_name FROM information_schema.tables
WHERE table_name IN (
'hydra_client_migration',
'hydra_jwk_migration',
'hydra_oauth2_authentication_consent_migration',
'hydra_oauth2_migration'
);
An empty result means you are not affected.
Admin JWK endpoints can now withhold private key material
The new configuration value jwks.admin_api.expose_private_keys controls whether admin /keys responses include private key
material:
jwks:
admin_api:
expose_private_keys: false
With false, GET /admin/keys/{set}, GET /admin/keys/{set}/{kid} and POST /admin/keys/{set} omit private key material and
return the public key instead. Set this for defense in depth if you do not read private keys through the admin API, so that admin
API access alone does not expose the keys that sign your tokens.
The default is true, which keeps the previous behavior of returning private key material for software-backed keys.
Keys are always stored in full. A key with no public representation — a symmetric key — is omitted from the response entirely, so
a response can come back with an empty keys array.
The two PUT endpoints are unaffected by the setting: they echo the key material the request carried, which discloses nothing the
caller did not already hold.
Keys backed by a Hardware Security Module are always returned as public keys, regardless of this setting. The public
/.well-known/jwks.json endpoint is unaffected. It has always returned public keys only.
Because reads then return public keys only, writing such a response back unchanged would replace your signing keys with their
public halves. With false, PUT /admin/keys/{set} and PUT /admin/keys/{set}/{kid} therefore reject a request that would leave
the key set without any private key, and answer 400. Retiring one key to its public half still works while the set keeps another
private key, and DELETE /admin/keys/{set} still removes a set.
Concurrent client secret rotations no longer hand back a dead secret
Rotating an OAuth 2.0 client secret twice at the same time could return a secret that never worked. Both requests read the client,
added their own secret, and wrote the whole record back, so whichever request finished last erased the secret the other had just
handed to its caller. The caller received a 200 OK with a secret that failed every token request from then on.
Rotations of the same client are now serialized in the database, so every secret a rotation returns stays valid until it ages out of the five-secret retention window or you replace the client's secrets explicitly.
If many rotations pile up on the same client at once, a request may now fail with 409 Conflict instead of succeeding with an
unusable secret. Retry the request.
Consent revocation no longer times out on large databases
Revoking consent sessions with DELETE /admin/oauth2/auth/sessions/consent could time out when the OAuth2 token tables were
large. The revocation now uses index-backed queries whose cost scales with the number of sessions being revoked instead of the
size of the token tables. No action is required.