New Ory Agent Security is now live! Claim your complimentary test drive. Get Started!

Skip to main content

v26.4.4

v26.4.4​

Deleting an OAuth 2.0 client with many tokens now succeeds​

Deleting an OAuth 2.0 client that had issued a large number of tokens or collected many consent sessions could fail, because everything belonging to the client was removed in a single database operation that ran out of time. Such a client could not be deleted at all.

Deletion now takes effect immediately and removes the associated data afterwards, in batches:

  • The client stops working the moment the request returns. It can no longer obtain tokens, its existing access and refresh tokens stop being valid, and remembered consent no longer applies to it. Stateless JWT access tokens are the exception: they are not stored, so they cannot be revoked and stay valid until their exp claim passes.
  • Its tokens and consent sessions are removed after that. Remembered login sessions aren't client-specific, so they remain after a client is deleted. If a deletion is interrupted, repeating the request continues where it stopped.

If you run Hydra yourself, the janitor takes a new --clients flag, which finishes any pending deletion and frees the deleted client's ID:

hydra janitor --clients {database-url}

Run it alongside the cleanups you already run. Without it, a deleted client keeps a row until its ID is reused, and its remaining tokens and consent sessions are not reclaimed.

Breaking changes​

Upgrading from a version earlier than v26.3.17 lets a deleted client keep working during the rolling upgrade. To upgrade safely, upgrade to v26.3.17 first.

The client ID is reserved after deletion until the client's remaining rows are gone, and for at least five minutes. Creating a client with that ID during the window fails with 409 Conflict and the error code client_id_reserved.

If you delete a client and immediately create a new one with the same ID — when resetting a client from a script, for example — retry the create until it succeeds instead of waiting a fixed time. A client that issued many tokens stays reserved until its cleanup finishes.

Writing a key uses the key ID of the URL path​

PUT /admin/keys/{set}/{kid} now creates or updates the key with the key ID from the URL path. Before, it used the kid of the request body and ignored the path, so a request could create or overwrite a key that the path didn't name. A request body without a kid stored a key without a key ID, which GET /admin/keys/{set}/{kid} and DELETE /admin/keys/{set}/{kid} couldn't address. Ory Hydra Authorization now stores such a key under the key ID of the path.

Breaking changes​

If the request body has a kid that differs from the key ID in the URL path, the request now fails with 400 Bad Request and the key set stays unchanged. Send the key ID of the key you write in the URL path.

Configure the Kratos integration under identity_provider.kratos​

Hydra now accepts admin_url, public_url, and headers under identity_provider.kratos. The existing urls.identity_provider settings remain supported as fallbacks for omitted fields. New headers replace the legacy header map. To stop sending legacy headers, remove the legacy setting.

Janitor request cleanup no longer scans the whole flow table​

hydra janitor --requests selects expired login and consent requests with a query that had no matching index, so every run read the entire hydra_oauth2_flow table. On large databases this took minutes.

A new partial index on hydra_oauth2_flow now serves that query. It is created by the hydra migrate sql up migration 20260921000001000000 and covers only rows the janitor can delete, so it stays small even when the table is large.

On PostgreSQL, CockroachDB, and YugabyteDB the index is built without locking the table, but the build reads the whole table once and can take a while on large installations. If you already created an index named hydra_oauth2_flow_janitor_requests_idx with the same definition, the migration leaves it in place. If a PostgreSQL build is interrupted, it leaves an invalid index behind that a later migration run does not rebuild. Check pg_index.indisvalid and run REINDEX INDEX CONCURRENTLY hydra_oauth2_flow_janitor_requests_idx to repair it. MySQL does not support partial indexes, so the migration is a no-op there.

Refreshing tokens now checks only the audiences that the client requested and the consent app granted against the client's current audience allow list. Audiences added by the consent app no longer cause refresh to fail when they are outside that list. Refreshed tokens retain the full granted audience.

For example:

  1. The client allows and requests audience A.
  2. The consent app grants A and B, adding B outside the client's allow list.
  3. The client refreshes its token.

Previously, refresh failed because granted audience B was not allowed. Now, refresh succeeds because requested audience A is still allowed. The refreshed access token retains A and B.

Removing a requested and granted audience from the client's allow list still makes refresh fail with invalid_request. To withdraw a consent-added audience, revoke the grant or refresh token.