DATE:
AUTHOR:
Ory Team
Ory Network Ory Hydra Ory Kratos Ory Keto Ory Oathkeeper Ory Polis Ory Enterprise License

Ory Network, Ory Hydra, Ory Kratos, Ory Keto, Ory Oathkeeper, Ory Polis v26.4.4 released

DATE:
AUTHOR: Ory Team

Ory Network

Match event stream events to your users by external ID

Identity, login, registration, recovery, settings, verification, and session events delivered through a live event stream now carry the identity's external ID in the ExternalID key of eventAttributes. Use it to match events to user records in your own system without reading the identity from the Ory API.

The key is absent when the identity has no external ID. Ory delivers it to your event stream only and does not keep it in its own analytics. Existing consumers keep working without changes.

Refresh tokens with consent-added audiences

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.


This is now available on Ory Network, for the Ory Enterprise License, and will be part of the next Ory Open Source release.


Ory Hydra

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.

Revoking consent revokes all of the subject's tokens

Revoking a subject's OAuth2 consent sessions now revokes every access and refresh token issued to that subject, or to that subject and client when you revoke consent for a single client. Before, it only revoked tokens issued for a consent session that still existed.

Tokens that never belonged to a consent session are now revoked too. This includes tokens from the resource owner password credentials grant and the JWT bearer grant, and tokens whose consent session was already removed.

Breaking changes

If you revoke consent for a subject and rely on that subject's password grant or JWT bearer tokens staying valid, those tokens now stop working. Issue new tokens after revoking consent, or use a different subject for tokens that must survive a consent revocation.

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.

Refresh tokens with consent-added audiences

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.


This is now available on Ory Network, for the Ory Enterprise License, and will be part of the next Ory Open Source release.


Ory Kratos

Webhooks and claims hooks can read labels of the caller's IP address

Every Kratos webhook, the password migration hook, and the session JWT claims hook now receive ctx.ip_labels, a sorted array of labels for the caller's IP address. It's always present, so a template written against the Ory Network keeps working on a self-hosted deployment.

Kratos reads the labels from request headers. Each of these headers set to 1 adds its label:

HeaderLabelX-Ory-Ip-Reputation-Anonymizerory:anonymizerX-Ory-Ip-Reputation-Open-Proxyory:open_proxyX-Ory-Ip-Reputation-Vpnory:vpnX-Ory-Ip-Reputation-Malwareory:malwareX-Ory-Ip-Reputation-Botnet-Ccory:botnet_cc

Without a reverse proxy that sets these headers, ctx.ip_labels is an empty array. If your proxy sets them, it must also remove them from every incoming request, or a caller can add labels to their own request.

Webhooks and claims hooks can read what is known about the caller's IP address

Every Kratos webhook, the password migration hook, and the session JWT claims hook now receive ctx.ip_labels. It lists the known services that the caller's IP address belongs to: ory:anonymizer, ory:open_proxy, ory:vpn, ory:malware and ory:botnet_cc. Ory labels the address at the edge, so you don't have to configure anything.

ctx.ip_labels is always present and is an empty array when the address is on none of the lists. To block a flow, raise an error in the template. To require a second factor, send the labels to a webhook on the login after position and let the webhook respond with required_aal. This template blocks sign-ins from known malware sources and sends the labels of all other sign-ins to the webhook:

function(ctx)
  if std.member(ctx.ip_labels, 'ory:malware') then
    error 'sign-in is not available from this network'
  else
    { identity_id: ctx.identity.id, ip_labels: ctx.ip_labels }

The webhook then responds with { "required_aal": "aal2" } when the labels contain ory:vpn. See Accessing the caller's IP labels and Require a second factor.


This is now available on Ory Network, for the Ory Enterprise License, and will be part of the next Ory Open Source release.


Ory Keto

Pass Keto configuration files explicitly

Ory Keto Permissions no longer loads ~/keto.yml automatically. Without --config, it uses configuration defaults and environment variables.

If you rely on the default file, add --config "$HOME/keto.yml" to your commands, for example keto serve --config "$HOME/keto.yml".


This is now available for the Ory Enterprise License and will be part of the next Ory Open Source release.


Ory Oathkeeper

Configure outbound HTTP clients per pipeline step and access rule

The cookie_session and bearer_token authenticators and the hydrator mutator reject upstream responses larger than 5 MiB and fail the request by default. Session check requests time out after 30 seconds by default. Set top-level http_client settings for shared defaults, then override individual fields in each handler's config.http_client or each access rule's config.http_client. Settings cover timeout, response size limit, retries, and private IP restrictions. They apply to session checks, OAuth token and introspection calls, remote and Keto authorization, hydrator calls, and HTTP JWKS fetching for JWT authentication and ID-token mutation. Use unlimited to disable the timeout or response size limit.

The hydrator's api.retry.give_up_after and api.retry.max_delay settings are deprecated and remain supported. Use http_client.timeout for the timeout and http_client.retry.max for the retry count.

Breaking changes

All pipeline HTTP clients limit response bodies to 5 MiB by default. Upstreams that return responses above 5 MiB require a larger http_client.max_response_body_size or unlimited. A hydrator give_up_after of 0s uses the HTTP client timeout, which defaults to 30 seconds, instead of disabling it.

Oathkeeper bounds how it fetches JSON Web Key Sets

The jwt authenticator, the id_token mutator, and the /.well-known/jwks.json endpoint fetch JSON Web Key Sets through one shared cache.

  • A key ID that no configured location contains triggers at most one fetch per location every 15 seconds, or every half jwks_ttl when that is shorter. A rotated key is picked up within that interval.

  • Past jwks_ttl, cached keys keep serving while Oathkeeper refreshes them in the background. If a location stays unreachable, its last keys are served for up to one hour past jwks_ttl and are then rejected until the location recovers.

  • A fetch runs for at most 15 seconds. Responses larger than 1 MiB and documents without a keys property are rejected and leave the cached keys in place.

Breaking changes

Oathkeeper no longer follows HTTP redirects when it fetches a JSON Web Key Set. If a jwks_urls or jwks_url entry redirects to another URL, replace it with the URL the redirect points to.


This is now available for the Ory Enterprise License and will be part of the next Ory Open Source release.


Ory Polis

Authorization codes, access tokens, and SSO sessions now expire on time

Ory Polis now checks whether a record has expired every time it reads one. Authorization codes, access tokens, and SSO sessions stop working as soon as their lifetime ends. Previously, expiry took effect when the periodic cleanup job removed the record. /oauth/token rejects an expired code with 403 Invalid code, /oauth/userinfo rejects an expired access token with 403 Invalid token, and an expired SSO session can no longer resume a login. Lifetimes are still set by DB_TTL, and the expires_in value that /oauth/token returns is unchanged.

Before you deploy this version, run the database migrations if you manage them yourself — on PlanetScale, or with DB_MANUAL_MIGRATION=true. This release adds an expiresAt column to the jackson_store table and every read uses it, so reads fail until the column exists. The migration changes only table metadata, so it is quick to apply to a live table. Deployments that let Ory Polis manage the schema add the column automatically at startup.

Batched directory sync events arrive in the order they happened

With DSYNC_WEBHOOK_BATCH_SIZE set, a connection's queued events could reach the webhook out of order, for example a group.user_added before the group.created of its group. This happened on DynamoDB once more events were queued than fit on one page of the database, and between events queued close together on SQLite, SQL Server, Redis and MongoDB. Each connection's events now go out in the order they were queued on every database engine, and a batch holds up to DSYNC_WEBHOOK_BATCH_SIZE events even when that is larger than DB_PAGE_LIMIT.

On DynamoDB a batch run now reads the whole event queue with strongly consistent reads, rather than each connection's events through an index that DynamoDB updates a moment after each write, so it sees every event queued before the ones it sends. Reads of the event queue use twice the read capacity they did.

Faster paged lists on SQL databases

Paged lists read from SQL databases, such as the lists of connections and directory sync users, sorted every record of their kind on each page, so a page took longer the more records there were. A new index on jackson_store (namespace, createdAt) lets the database read a page in order instead. It replaces the index on jackson_store (namespace), which the new one covers. On PostgreSQL, CockroachDB and YugabyteDB, lists read through a secondary lookup get the same treatment: jackson_index (key, id) replaces jackson_index (key).

If you run migrations yourself (DB_MANUAL_MIGRATION), run them before you upgrade. On PostgreSQL the migration builds the index with CREATE INDEX CONCURRENTLY, and MySQL, MariaDB, CockroachDB and YugabyteDB build it without blocking writes. Otherwise Polis creates the index when it starts, and on PostgreSQL, SQL Server and SQLite writes to jackson_store wait until it is built: about a second per million rows on PostgreSQL.

On YugabyteDB whose tables Polis creates when it starts, create the indexes yourself before you upgrade, with CREATE INDEX CONCURRENTLY IF NOT EXISTS "_jackson_store_namespace_created" ON "jackson_store" ("namespace", "createdAt") and CREATE INDEX CONCURRENTLY IF NOT EXISTS "_jackson_index_key_id" ON "jackson_index" ("key", "id"). Polis creates them inside a transaction, where YugabyteDB cannot build an index concurrently, and a nonconcurrent build is not safe while Polis writes to the table.

Setup links can give their SAML connections their own ACS URL

With ACS_URL_HOST configured, an SSO setup link can now carry an ACS URL ID, so the SAML connections created through it get their own ACS URL. Tick the new option on the create Setup Link page, or pass acsUrlId to POST /api/v1/sso/setuplinks. The setup link's instructions show that ACS URL and a matching SP metadata link, so your customer can configure their IdP before the connection exists. Regenerating or renewing the link keeps the ID. Links created without it work as before.


This is now available for the Ory Enterprise License and will be part of the next Ory Open Source release.


Ory Elements and Ory Terraform

No significant changes in this release.

Powered by LaunchNotes