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

Skip to main content

Kratos

v26.3.18​

Require sign-in with a specific OpenID Connect provider from the after web_hook response​

A login after web_hook with response.parse enabled can now return {"force_credential": {"method": "oidc", "provider": "<provider id>"}} to require that session to be signed in with one specific OpenID Connect provider. Use it when an external check, such as a bank ID broker or an ID verification provider, has to confirm who the user is before they can continue. One response can raise the AAL, update the identity, and require a provider at the same time; the requirements are independent.

The requirement is stored on the session, so it outlasts the login that created it. While it is pending, /sessions/whoami, GET /sessions, and the settings flow answer with 403 Forbidden and the new error ID session_credential_required, whose error.details.redirect_browser_to points at a refresh login. That login offers only the required provider's sign-in button. Signing in there clears the requirement; signing in with any other credential does not. The user does not need an account at the provider yet: the required sign-in links the provider account, and the tokens it issued, to the identity. Because it can link an account, the required sign-in takes the same Authenticator Assurance Level as the settings flow, so a session that still owes a second factor is sent to it first.

This is off by default and uses the existing feature_flags.webhook_response_directives feature flag, or kratos_feature_flags_webhook_response_directives on the project for Ory Network. While the flag is off, a force_credential field in a web_hook response is ignored. Once it is on, oidc is the only supported method, and a value that names another method or no provider fails the login rather than being silently dropped. A provider that is not configured or is scoped to an organization fails the login with a configuration error, and so do two web_hooks on the same position that require different providers. On a web_hook at another position the directive is logged and ignored.

Webhook failures now carry an error ID​

Webhook request timeouts now return error.id: webhook_timeout. Failed requests, including exhausted retries on HTTP 429 or 5xx other than 501, return webhook_request_failed. A 4xx response other than 429, or a 501 response, returns webhook_unexpected_status unless response parsing is enabled and the body contains valid validation messages; those messages still appear as validation errors.

Status codes and messages are unchanged except for parsed 4xx responses other than 429, and parsed 501 responses, that have no valid validation messages. These now return 502 Bad Gateway with webhook_unexpected_status instead of a generic parsing error. For API registration flows, the previous response was HTTP 400 with an internal-error body.

These IDs don't cover every webhook failure. Empty or malformed HTTP 200 responses with response parsing enabled still fail without a webhook error ID.

Webhooks and claims hooks can read the caller's location​

Every Kratos webhook now receives the caller's approximate location as ctx.location, with the fields city, region, country, latitude and longitude. It's available on all self-service webhooks and on the password migration hook.

ctx.location and all of its fields are always present, so a template written against the Ory Network keeps working on a self-hosted deployment. Kratos derives the values from the Cf-* request headers, so on a self-hosted deployment the strings are empty and the coordinates are null unless your own proxy sets those headers.

Filling the fields is your responsibility. Ory ships no geolocation database, so the values come from a reverse proxy in front of Ory that can resolve an IP address to a location — for example nginx with the ngx_http_geoip2 module and a MaxMind database. Set Cf-Ipcity, Cf-Region-Code, Cf-Ipcountry, Cf-Iplatitude and Cf-Iplongitude.

Set all five, including any you can't resolve: nginx's proxy_set_header replaces whatever the client sent, and it omits a header whose value is empty. A header you leave alone arrives from the client unchanged, and you must not use these fields for authorization decisions.

The webhooks guide has a worked nginx configuration.

The session JWT claims hook is a payload change rather than a template binding: its request body gains a location object. Existing endpoints that ignore unknown fields are unaffected.


v26.3.17​

End Ory sessions when the upstream OpenID Connect provider signs the user out​

Social sign-in providers can now end the Ory session they created, using OpenID Connect Front-Channel Logout 1.0. Set front_channel_logout: true on the provider, and register https://<your-ory-domain>/self-service/methods/oidc/frontchannel-logout/<provider-id> as that provider's front-channel logout URL with frontchannel_logout_session_required enabled. Each provider has its own logout URL, and can only end sessions created through itself. When the user signs out at the provider, Ory revokes the session that sign-in created.

The provider must return the sid claim in the ID token, and the provider's claims_source must be id_token (the default). The mechanism relies on third-party cookies and therefore only works in browsers that still allow them. Nothing changes for providers that leave front_channel_logout unset, and the Ory session cookie's own SameSite behavior is unchanged.


v26.3.16​

Phone number validation now uses current numbering-plan data​

Ory validates phone numbers against the current libphonenumber dataset. Numbers in ranges assigned since the previous dataset now validate on format: tel identity traits and on SMS identifiers, where they were previously rejected.

Phone numbers typed with any Unicode decimal digits, such as Devanagari or Bengali, now normalize to E.164. Previously only Arabic-Indic and fullwidth digits did.

Breaking changes​

The same refresh removes ranges that national regulators have withdrawn:

CountryWithdrawn range
Syria+963 91x, +963 92x, +963 97x
Slovenia+386 49x
Monaco+377 49x, +377 049x
Faroe Islands+298 88x
Vietnam+84 80xx
Norway / Svalbard+47 4536x
Georgia+995 5441x

Syria also gains the +963 90x range.

For identities that already hold a number in a withdrawn range:

  • Sign-in, account recovery, and verification work only when the number is entered without spaces or dashes: +38649xxxxxx, not +386 49x xxx. Formatted input fails with the generic invalid-credentials error, so the cause is not visible to the user.
  • New registrations with such a number are rejected.
  • Password changes and other settings updates are rejected while the stored number remains in a withdrawn range, because the identity is validated against the schema on every settings write.

Check your identities for the prefixes above and ask affected users to update to a currently assigned number. The withdrawn ranges are no longer assignable, so those numbers may stop receiving SMS regardless of Ory.


v26.3.15​

Oathkeeper adopts shared HTTP metrics and adds per-rule access decisions​

Oathkeeper uses the shared Ory HTTP metric schema. It also exposes ory_oathkeeper_access_decisions_total, which reports allowed requests, authentication failures, authorization failures, and pipeline errors by rule ID. Access decisions are independent of login redirects and upstream response codes.

Breaking changes​

For Oathkeeper dashboards and alerts, replace service with app and status_code with code. The request label is replaced by endpoint, which contains API router patterns such as /rules/{param} and /decisions/ and is empty for proxy traffic. The app values remain oathkeeper-api and oathkeeper-proxy. HTTP metrics also include version, hash, and buildTime. Metric names retain the configured prefix. Health requests are now included.

serve.prometheus.hide_request_paths and serve.prometheus.collapse_request_paths are deprecated and have no effect. Update path filters to use router patterns or the new per-rule counter.

In Oathkeeper, Kratos, Hydra, and Keto, the requests_statuses_total metric's method labels now follow the same normalization as other shared HTTP metrics: recognized methods use lowercase values, and other methods use other. Oathkeeper applies this normalization to all HTTP metrics.

Unsupported methods use other instead of unknown across all shared HTTP metrics. Update queries that filter on the previous method label.

Sensitive values are now encrypted by default​

Kratos used to default to the no-op cipher, which stored sensitive values in plaintext — OIDC provider tokens, and DeviceAuthn PIN secrets in Ory Enterprise License deployments. ciphers.algorithm now defaults to [xchacha20-poly1305, noop]: new values are encrypted with XChaCha20 Poly1305, and values already stored in plaintext keep decrypting until they are written again. An unknown algorithm no longer falls back to plaintext; it fails closed, and every encrypt or decrypt returns an error.

Breaking changes​

secrets.cipher is now required. Kratos refuses to start unless it holds at least one 32-character secret. Add it to your configuration before you upgrade — the current version accepts the setting and ignores it, so the rollout is:

  1. Add a 32-character secrets.cipher.
  2. Roll out this version.

To keep storing these values in plaintext, set ciphers.algorithm: noop explicitly. This provides no confidentiality or integrity and is not recommended. secrets.cipher is required either way.

During the rolling upgrade, instances still running the previous version can't read values that upgraded instances have already encrypted. If you can't tolerate that window, set ciphers.algorithm: noop explicitly in step 1 and remove it once every instance runs the new version. Both instances then agree on the algorithm throughout the rollout, and the switch happens on a configuration change rather than across the deployment.

Changing the algorithm​

ciphers.algorithm also accepts an ordered list. The first algorithm encrypts new values; the rest only decrypt older ones, and each value is re-encrypted with the first algorithm on its next write. This mirrors secrets.cipher key rotation, one level up, so you can change algorithms without re-encrypting existing data and without downtime. noop is only valid as the last entry, because it decodes any value and would shadow the algorithms after it.

Import identities faster with all-or-nothing batches​

PATCH /admin/identities accepts a new with_partial_inserts field in the request body. It defaults to true, which is the existing behavior: each identity is imported on its own, and an identity that conflicts with one that already exists is reported individually while the rest of the batch is still imported.

Set it to false to import the batch as a single unit. The whole batch is then written in one transaction, which is considerably faster. In a cluster whose regions are 250 ms apart, a batch of 10 identities that took 2.2 seconds takes 0.4 seconds, and a batch of 1000 that took 7 seconds takes 2.5. In exchange, a single conflict fails the request with 409 Conflict and no identity from the batch is created.

Use it when you expect the batch to contain no duplicates. That is the case when you import from a database that already enforces the same unique constraints, or when you have validated the data beforehand. You can also use it optimistically: send the batch with with_partial_inserts set to false, and repeat the request with true if it returns 409 Conflict.

The response does not say which identities conflicted, because none of them were imported. The retry with with_partial_inserts set to true imports every identity that does not conflict and reports a per-identity error for the ones that do.

Support IdP-initiated SAML login​

Enterprise users can now start a login from their identity provider's app launcher (for example the Okta or Azure AD dashboard) and land in your application with a valid Ory session.

When an identity provider posts an unsolicited SAML response, Ory Polis validates it and redirects the browser to a new Kratos entry point with a single-use authorization code. Kratos starts a regular SP-initiated login flow — with the usual state, CSRF, and PKCE protections — and forwards the code as a code_hint to the Polis authorize endpoint. Polis consumes the hint and completes the flow immediately, without a second round-trip to the identity provider.

Standalone Ory Polis already supports IdP-initiated SAML login through its direct redemption flow. This change adds the Kratos side: an entry point that folds the unsolicited response into a regular SP-initiated Kratos login, wired together with Polis through the code_hint.

To enable it on Ory Network, turn on the "IdP-initiated login" toggle in the SAML provider settings in Ory Console. Ory then points the SAML connection's default redirect URL at the entry point automatically.

To enable it on a self-hosted Ory deployment that runs Kratos and Polis together:

  1. Set idp_initiated_login_enabled: true on the SAML (jackson) provider in your Kratos configuration.
  2. Point the Polis SAML connection's defaultRedirectUrl at https://<your-project>/self-service/methods/saml/idp-initiated/<provider>.
  3. Run Polis with IDP_ENABLED=true.

IDP_ENABLED is the deployment-wide switch. A connection follows it unless it sets idpInitiatedLoginEnabled explicitly: true always allows IdP-initiated login for that connection and false always blocks it. On Ory Network the switch stays off and the Console toggle sets the per-connection flag, so no deployment-wide change is needed.

The code hint is single use, expires with the code store TTL, and is only valid for the exact connection it was issued for. SP-initiated codes cannot be used as hints. If a hint is invalid or already consumed, the user is sent through the regular SP-initiated redirect instead — usually completing silently against the identity provider's existing session.

TLS configuration and unix socket recovery in Hydra, Keto and Kratos​

A configured certificate is now the only switch for TLS in all three services. serve.<endpoint>.tls.enabled is deprecated but still accepted so that an existing configuration keeps loading, but it has no effect. Each service logs a warning at startup naming the key to remove. To serve plain HTTP behind a proxy that terminates TLS, remove the certificate rather than the flag.

Hydra and Kratos also pick up two fixes from the shared serve configuration:

  1. configuring only one half of a certificate pair is now a startup error instead of a listener that silently falls back to plain HTTP
  2. a unix socket left behind by an unclean shutdown is replaced instead of blocking the next start.

Because the three services now configure their endpoints identically, the documentation for it is one page: Serving the APIs. It replaces the separate Hydra and Kratos HTTPS pages, which now redirect to it.


v26.3.14​

One-time code sessions report the delivery channel​

Sessions created with a one-time code now record whether the code was sent by email or SMS. The code entry in authentication_methods carries a channel field set to email or sms, wherever the session is returned: /sessions/whoami, the admin session endpoints, and webhook payloads.

No other authentication method sets the field, so responses for password, social sign-in, and second-factor methods are unchanged.

Set the size limit for Jsonnet mapper input​

Ory Identities renders Jsonnet mappers — OIDC claims mappers, webhook bodies, courier templates — in a sandboxed worker process, and the input it sends that worker is limited to 1 MiB. A sign-in carrying a large context can exceed that limit, most often an OAuth2 or OIDC sign-in with many headers, many cookies, or a long return_to URL. That evaluation fails, and nothing else is affected.

The limit is now configurable. Set jsonnet.max_input_size to a byte count:

jsonnet:
max_input_size: 4194304

or set the JSONNET_MAX_INPUT_SIZE environment variable to the same value. The default is unchanged at 1 MiB, so a deployment that sets nothing behaves as before. The largest accepted value is 4 MiB, which is the size the worker reads with.

Raise it only as far as you need. One worker serves every evaluation in the process and keeps enough memory to hold the largest input it has read.


v26.3.13​

Creating an identity with an unknown schema ID now fails with 400​

Sending a schema_id that your project does not define to POST /admin/identities, PUT /admin/identities/{id}, or PATCH /admin/identities/{id} now returns 400 Bad Request with the reason Unable to find JSON Schema ID: <id>. It previously returned 500 Internal Server Error.

You are affected if your integration treats these endpoints' errors by status class — for example by retrying on 5xx. Requests carrying an unknown schema ID were never going to succeed, and are now reported as the client error they are, so they stop being retried. The response for a schema ID your project does define is unchanged.

Listing identities by ids returns every match in one response​

GET /admin/identities?ids=... is a bulk lookup, not a listing. It now returns every identity you asked for in one response and sends no pagination headers.

Self-hosted deployments previously paginated this request at 100 ids by default, so a caller that asked for 300 ids and did not follow the Link header silently received 100 identities. Pass at most 500 ids per request, as before. If you read X-Total-Count or follow Link on this request, drop that handling — the response is complete as it stands.

On the Ory Network, these identities now come back in the same shape as GET /admin/identities/{id}: each carries its schema_url, which this filter previously left empty, and legacy credentials are upgraded to their current representation. Identities are also validated, so a request that matches an identity whose schema_id no longer resolves to a configured schema now fails instead of returning that identity with an empty schema_url.

Raise a login's AAL from the after web_hook response​

A login after web_hook with response.parse enabled can now return {"required_aal": "aal2"} to require step-up for that login. One response can both update the identity and raise the AAL. Users who do not yet have a second factor are routed to enrollment rather than let through.

This is off by default. Enable it with the new feature flag feature_flags.webhook_response_directives, or on Ory Network by setting kratos_feature_flags_webhook_response_directives on the project. While the flag is off, a required_aal field in a web_hook response is ignored exactly as it is today, so existing hooks are unchanged. Once it is on, a required_aal value other than "aal2" fails the login rather than being silently dropped; on a web_hook at another position the directive is logged and ignored.

Route generic social sign-in with an organization-claimed email to the organization's SSO​

Signing in with a generic social sign-in provider (for example Google) using an email address whose domain is claimed by an organization no longer fails with an error. Kratos now responds with a login flow scoped to the matched organization that contains only the organization's SSO provider buttons and an informational message (ID 1010026), matching the identifier-first behavior. This covers both existing accounts and first-time sign-ins.

SCIM group pushes with large memberships are batched​

Pushing a group with a large membership through SCIM no longer times out. A PUT, PATCH, or DELETE on /scim/{client}/v2/Groups/{id} used to process members one at a time, which past a few hundred members exceeded the request budget and rolled the write back, leaving the identity provider with a group that had been emptied. Members are now processed in batches. Very large pushes can still exceed the request budget, especially on multi-region deployments.

Error responses on these endpoints changed:

  • A transient failure while processing a member now answers 500 Internal Server Error instead of 409 Conflict. A 409 tells an identity provider to stop retrying, so pushes that would have succeeded on a retry were abandoned. Only a genuine unique-attribute collision still answers 409 with scimType: uniqueness.
  • A push that would change more than 12,000 members is rejected up front with 400 Bad Request and scimType: tooMany, naming the group and the limit. Split the group into smaller ones to provision it.
  • A push whose membership includes an identity that was not provisioned through SCIM answers 400 Bad Request with scimType: invalidValue and names the identities, instead of a generic 500. Provision those identities through /Users first.

Session-to-JWT key set hardening and load failures returning 400​

When Ory converts a session to a JSON Web Token, a key set loaded from the tokenize template's jwks_url at a file:// location is now cached for up to 3 minutes instead of up to one hour, so a rotated key file is picked up faster. If the file becomes unreadable, Ory keeps signing with the previously loaded keys for up to one hour before requests fail, where previously requests failed as soon as the cached keys expired. Key set documents larger than 1 MiB are rejected.

A jwks_url whose key set cannot be loaded now fails the request with 400 Bad Request instead of 500 Internal Server Error, because it is a defect of the selected tokenize template.

TOTP authenticator setup no longer breaks after a rejected settings submission​

Enrolling a TOTP (authenticator app) two-factor method could fail permanently when the settings flow used a webhook that can interrupt the request. After the first interrupted submission, every retry on the same flow returned an internal server error, and the user could not finish setup without starting a new flow.

TOTP setup now stays usable after an interrupted submission: the user can submit again on the same flow and is given a fresh secret and QR code to scan when the previous one is no longer available.


26.3.12

This version contains only minor changes and improvements such as dependency updates.


26.3.11

This version contains only minor changes and improvements such as dependency updates.


26.3.10

This version contains only minor changes and improvements such as dependency updates.


26.3.9

This version contains only minor changes and improvements such as dependency updates.


v26.3.8​

OIDC-scoped after hooks now run for native id_token logins​

Login and registration hooks scoped to the oidc method now also run when a native app completes the flow by submitting an id_token directly (for example Sign in with Apple on iOS). Previously these flows only triggered the unscoped hooks. If you use method-scoped hooks, expect them to fire for these logins from now on. The flow's active method is now also set to oidc in hook payloads and events for these flows.


v26.3.7​

Force sessions to AAL2 with session.whoami.required_aal​

Setting session.whoami.required_aal: aal2 now forces every session to reach AAL2 before it is treated as valid. Users who already have a second factor are sent through the existing ?aal=aal2 step-up. Users who do not have a second factor are redirected to the settings flow to enroll one, instead of being silently allowed through.

While a session still owes a second factor, its settings flow becomes enrollment-only: it carries a new UI message (ID 1050024) telling the user to set up a second factor, offers only the second-factor enrollment methods (TOTP, lookup secrets, and WebAuthn when configured for MFA), and rejects a submission for any other method. Native and API clients receive a show_settings_ui continue_with item on the login response and a new error ID session_aal2_enrollment_required alongside the existing session_aal2_required. The required_aal setting on the settings flow itself is unchanged and still accepts only aal1 or highest_available.

GET /sessions continues to require the configured AAL, as it did before. DELETE /sessions and DELETE /sessions/{id} deliberately do not: signing other devices out is what a user needs after losing a second factor, so revocation stays available to a session that has not yet met the requirement. Such a revocation is recorded in the log with the configured required_aal and the session's own AAL.


26.3.6

This version contains only minor changes and improvements such as dependency updates.


v26.3.5​

Passkey display names now follow one deterministic precedence rule​

When an identity schema flags more than one trait with passkey.display_name: true (for example email and phone), the settings flow previously picked the display name for a new passkey at random from the populated flagged traits. A user adding a passkey from account settings would sometimes see it named after their email and sometimes after their phone number.

The display name is now resolved by a single rule in every flow: traits flagged with passkey.display_name take precedence over traits flagged with webauthn.identifier, and within each group the alphabetically-first trait with a non-empty value wins. With email and phone flagged, the email address is now always used when it is set.

For schemas that flag both passkey.display_name and webauthn.identifier traits, this also changes the candidate order the registration flow publishes to the browser: passkey.display_name traits now come first, where previously all flagged traits were sorted alphabetically in one list. A passkey created during registration may therefore get its name from a passkey.display_name trait where an alphabetically-earlier webauthn.identifier trait was used before. Existing passkeys keep their names, and schemas that flag only one kind of trait see no change at registration.

Prometheus metrics for the Jsonnet process pool​

The Prometheus endpoint now reports the state of the Jsonnet worker process pool, which evaluates webhook payload templates, OIDC claims mappers, and session token claims. Use these metrics to see whether requests are queueing for a free worker.

The metrics mirror the existing SQL connection pool metrics and carry no labels, because each process holds exactly one pool:

MetricTypeDescription
ory_jsonnet_pool_max_processesgaugeMaximum number of worker processes.
ory_jsonnet_pool_processesgaugeStarted worker processes, in use and idle.
ory_jsonnet_pool_in_use_processesgaugeWorker processes currently in use.
ory_jsonnet_pool_idle_processesgaugeIdle worker processes.
ory_jsonnet_pool_constructing_processesgaugeWorker processes currently starting.
ory_jsonnet_pool_wait_count_totalcounterAcquires that had to wait for a worker.
ory_jsonnet_pool_wait_duration_seconds_totalcounterTotal time blocked waiting for a worker.
ory_jsonnet_pool_acquired_processes_totalcounterWorker processes ever acquired.
ory_jsonnet_pool_acquired_duration_seconds_totalcounterTotal duration of all acquires.
ory_jsonnet_pool_canceled_acquires_totalcounterAcquires canceled before a worker was free.

The courier now evaluates Jsonnet in an isolated process​

The courier renders the request body of an HTTP delivery channel from a Jsonnet template, and SMS delivery always uses an HTTP channel. It was evaluating those templates inside the courier process instead of in a sandboxed worker, so a template that consumed excessive memory or CPU affected message delivery as a whole. The courier now uses the same isolated worker pool as the rest of Kratos, where evaluation runs under a memory limit and a filesystem sandbox.

No configuration changes are needed, and template behavior is unchanged.

Ory Kratos now serves the WebAuthn Related Origin Requests document at /.well-known/webauthn. It lists the relying party origins configured for the enabled WebAuthn and passkey methods.

Browsers that support Related Origin Requests (Chrome 128+, Safari 18+) consult this document when a page requests a WebAuthn relying party ID that does not match the page's own origin. This lets passkeys bound to a relying party ID served by Ory work on additional origins, for example across several custom domains: add the extra origins to the relying party origins of the WebAuthn or passkey configuration and they appear in the document automatically.

See https://www.w3.org/TR/webauthn-3/#sctn-related-origins for details.

Webhooks now receive enrolled credential types and available AAL​

Login, registration, settings, recovery, and verification webhooks now receive the identity's enrolled credential types and available authenticator assurance level. The payload's identity.credentials lists each enrolled credential's type and identifiers, plus its version and its creation and update timestamps when known — never secrets: credential configuration is stripped and identity-provider (OIDC and SAML) credentials list no identifiers. identity.available_aal reports whether the identity has a usable second factor. Use this to build risk-based skip-vs-challenge decisions in your webhook.


v26.3.4​

Account linking now works when registration is disabled​

Automatic account linking now works when registration is disabled. Signing in with an OIDC or SAML provider whose email matches an existing account starts the account linking flow even if selfservice.flows.registration.enabled is false. New sign-ups remain blocked. Previously, the linking attempt failed with "Registration is not allowed because it was disabled."

Bound the password identifier-similarity check by length​

The password validator's identifier-similarity check now bounds the length of the identifier and password it compares. Real identifiers and passwords are far shorter than this bound, so legitimate flows are unaffected. This hardens the check against excessive resource use on very long inputs.

Exclude already registered credentials in WebAuthn and passkey settings​

The WebAuthn and passkey settings flows now populate excludeCredentials in the registration challenge with the identity's already registered credentials. Browsers no longer silently overwrite an existing platform-authenticator entry (Touch ID, Windows Hello, Android) when a user re-enrolls the same device; instead the authenticator reports the credential as already registered. Kratos additionally rejects a registration response whose credential ID is already registered on the identity.

Metrics endpoints now support OpenMetrics and trace exemplars​

The Prometheus metrics endpoints of Kratos, Keto, Hydra, Oathkeeper, and Talos now additionally serve the OpenMetrics exposition format. The format is selected automatically through standard HTTP content negotiation: scrapers that ask for OpenMetrics receive it, and all other scrapers keep receiving the classic text format. No configuration or scraper change is necessary.

OpenMetrics enables exemplars, which attach a trace reference to a metric sample. Two sets of metrics use this:

  • The HTTP request duration histograms and request counters attach an exemplar when a request runs under a sampled trace, so latency dashboards can link slow-request outliers and error spikes to the traces that caused them.
  • The ory_x_popx_cockroach_transaction_retries_total counter attaches an exemplar whose value is the transaction's retry count when a transaction is retried during a traced request, and the trace records a db.transaction.retry span event. Dashboards can link retry spikes directly to the traces that caused them.

That counter's caller label now also more reliably attributes automatic CockroachDB transaction retries to the business operation that opened the transaction, for example the OAuth2 refresh token flow, instead of a generic persistence Transaction wrapper method. This makes it possible to see which operation is causing database contention. Dashboards or alerts that match specific caller label values may need updating, because existing label values change with this release.

Opt-in cleanup of expired data on CockroachDB​

Ory Kratos Enterprise License deployments on CockroachDB can now set up row-level TTL jobs that automatically delete expired data. Run the new kratos migrate row-ttl up command to configure these retention periods:

  • Self-service flows, one-time codes, tokens, and continuity containers: 7 days after they expire.
  • Sessions: 30 days after they expire.
  • Courier messages, message dispatches, and self-service errors: 30 days after they are created.

The command is opt-in: kratos migrate sql up does not change TTL settings. Use kratos migrate row-ttl dump to print the SQL statements — for review, or to adapt the retention periods and apply them manually. kratos migrate row-ttl status shows which TTL migrations are applied, and kratos migrate row-ttl down removes the TTL configuration again.

Running kratos migrate row-ttl up replaces any custom row-level TTL settings on the affected tables with the defaults above. The command requires CockroachDB and exits with an error on other databases.

CockroachDB deletes expired rows on a schedule, not at the exact moment the retention period ends. Expired rows can remain in the database for some time: either the TTL job has not processed them yet, or it has deleted them but garbage collection has not yet removed them.

Persist WebAuthn and passkey sign counter for clone detection​

Ory now updates the stored WebAuthn and passkey authenticator state after every successful login. The signature counter, clone-warning flag, and backup state are written back to the credential, so W3C WebAuthn clone detection works as intended.

Previously the signature counter stayed at its registration value, which left clone detection inert. A login that presents a non-increasing counter is still allowed — synced passkeys (such as Apple iCloud or Google) legitimately report a static counter — but the possible-clone warning is now recorded on the credential and is visible through the admin identity API.

Revoke a single DeviceAuthn key by identifier​

DELETE /admin/identities/{id}/credentials/deviceauthn now revokes a single DeviceAuthn key selected by the identifier query parameter, where the identifier is the key's client_key_id. This is the same parameter that OIDC and SAML credential deletion already use.

Because the client_key_id is unique per identity, administrators can revoke exactly one device key without affecting other keys — including keys that share the same device name. Before, deleting the deviceauthn credential type removed all device keys at once.

The endpoint returns 404 Not Found when the identity has no deviceauthn credential or when no key matches the identifier, and 400 Bad Request when the identifier is missing. Removing the last key leaves the deviceauthn credential with no keys.

Find the client key ids by calling GET /admin/identities/{id}?include_credential=deviceauthn.

Second-factor enrollment now refreshes the session's authenticated_at timestamp​

Setting up a second factor — authenticator app (TOTP), security key, passkey, or lookup secret — in a settings flow now refreshes the session's authenticated_at timestamp, in addition to elevating the session to AAL2. Linking a social sign-in provider in a settings flow also refreshes the timestamp.

Previously, enrolling a second factor raised the session to AAL2 but left authenticated_at unchanged. Because AAL2 re-authentication prompts are timed from authenticated_at, users could be asked to re-authenticate immediately after proving possession of the factor they had just enrolled. The timestamp now reflects the enrollment as the most recent authentication event.

YugabyteDB support for Kratos, Hydra, and Keto (OEL)​

Ory Enterprise License deployments can now run Ory Kratos, Ory Hydra, and Ory Keto against YugabyteDB.

Enabling YugabyteDB​

Point the dsn at your YugabyteDB cluster using the yugabyte:// scheme (the synonym yugabytedb:// also works):

yugabyte://user@host:5433/database?sslmode=disable

Recovering an interrupted migration​

YugabyteDB applies DDL with autocommit. Old PostgreSQL migrations inherited by YugabyteDB can contain multiple statements, so an interruption may leave the first statements applied without a row in schema_migration. Current versions of Ory no longer use multi-statement migrations and are safe.


v26.3.3​

Kratos gains a FIPS 140-3 build with a PBKDF2 password hasher​

Ory Enterprise License releases now include a dedicated FIPS build of Kratos (kratos_oel_fips_<version>_<os>_<arch>) next to the standard build. The FIPS build uses the Go Cryptographic Module and starts with GODEBUG=fips140=on by default, so all cryptographic operations run through the FIPS 140-3 validated module. The standard build is unchanged.

PBKDF2 password hasher​

You can now choose pbkdf2 as the password hashing algorithm alongside argon2 and bcrypt. PBKDF2-HMAC-SHA256 is FIPS 140-3 approved, which makes it the recommended choice for FIPS-compliant deployments. Configure it under hashers:

hashers:
algorithm: pbkdf2
pbkdf2:
iterations: 600000
salt_length: 16
key_length: 32

The defaults are 600,000 iterations, a 16-byte salt, and a 32-byte derived key.

Existing password hashes keep working​

Passwords hashed with a different algorithm still verify as before. When an identity signs in with such a password, Kratos re-hashes it with the configured algorithm on the next successful login, just as it already does when you switch between argon2 and bcrypt. No migration or password reset is needed.

FIPS mode log signals​

When the FIPS build runs, Kratos tells you where your configuration deviates from a FIPS-compliant setup:

  • On startup, Kratos logs that FIPS 140-3 mode is active.
  • On startup, Kratos warns when the configured password hasher or cipher is not FIPS 140-3 approved (any hasher other than pbkdf2, or any cipher other than aes, including the default noop).
  • When signing a session JSON Web Token through the JWT webhook, Kratos warns if the signing algorithm is not FIPS 140-3 approved (once per algorithm). Configure a JSON Web Key with an approved algorithm (RS*, PS*, ES*, HS*, or EdDSA) for a fully FIPS-compliant deployment.

These warnings do not block any operation.

One-time-code and verification messages can be branded per OAuth2 client​

The login_code_valid, registration_code_valid, and verification_code_valid courier templates (email and SMS), as well as the link method's verification_valid email template, now include a scoped oauth2_login_request object when the flow was started through an OAuth2 login challenge. Verification flows inherit the challenge from the OAuth2-initiated registration or login that triggered them. The object contains the challenge and the initiating client's client_id, client_name, client_uri, logo_uri, and metadata. Custom HTTP mail servers can brand these messages per OAuth2 client from template_data.oauth2_login_request.client — a trusted, server-sourced value — without relying on the end-user-controlled transient_payload or resolving the login challenge with an unscoped API token. Flows without a login challenge are unchanged.

Stricter validation of iOS device attestations at enrollment​

When you enroll an iOS device key, Kratos now inspects the authenticator data of the Apple App Attest attestation and rejects keys that fail any of these checks:

  • The attestation environment (AAGUID) must be the production environment. Development keys, which can be produced on jailbroken devices, are accepted only when relaxed attestation is enabled, and such keys remain short-lived.
  • The signature counter must be zero, confirming the key was just created and has never been used.
  • The credential ID embedded in the attestation must match the key identifier the client submits.

These checks harden the enrollment flow against forged or reused attestations. Compliant iOS clients are unaffected.


v26.3.2​

Enforce a DNS-label boundary on wildcard return URLs and CORS origins​

A wildcard in a return URL or CORS origin is now honored only when the * is confined to a subdomain label and the host part after it is a registrable domain (an eTLD+1, such as https://*.example.com or https://*.example.co.uk). The wildcard then matches only hosts under that one customer-owned domain.

These forms are no longer honored, because each can match an attacker-registrable host:

  • dot-less, e.g. https://*foo.com (matches https://evilfoo.com);
  • public-suffix base, e.g. https://*.com, https://*.co.uk, https://*.vercel.app;
  • trailing or bare, e.g. https://www.example.*, https://*.

Only the host is checked: wildcards in the URL path are unaffected. Exact origins, the catch-all *, and Hydra's https://* ("all hosts for a scheme") are unchanged.

What enforces it​

  • Return URLs: Kratos drops unsafe wildcard allowed_return_urls entries when it loads its config, and they never match.
  • CORS origins: matching across Kratos, Hydra, Keto, Oathkeeper, and Talos enforces the same boundary at request time.

Breaking changes​

Replace any unsafe wildcard with the labelled https://*.example.com form, or list exact origins. Update serve.*.cors.allowed_origins and Kratos allowed_return_urls before upgrading; otherwise cross-origin requests and post-flow redirects from those hosts will be blocked.

First-factor device login with an app PIN​

Enrolled mobile devices can now complete a full AAL2 login in one step using an app PIN or platform biometrics, without a password or SMS code. The PIN is never transmitted or stored: it unlocks an on-device secret, and the server verifies a proof bound to the device's hardware-backed key. After the configurable number of wrong PIN attempts (default 5) the key locks and the user must recover through the fallback login.

New selfservice.methods.deviceauthn.config options:

  • first_factor — enable first-factor device login (default false).
  • pin_max_attempts — number of wrong PIN attempts before the key locks (default 5).
  • ios_biometric_first_factor — allow platform biometrics as a first-factor credential on iOS (default false). iOS needs this explicit opt-in because App Attest cannot prove that the key is biometric-gated; Android platform keys are attestation-verified and need no opt-in.

Breaking change: device keys enrolled before this release carry no user_verification and can no longer authenticate. Step-up login rejects them with a dedicated "enroll your device again" message, and they no longer count as active second factors, so affected accounts keep their first-factor login and can re-enroll the device in settings.

Faster conflict detection during OIDC login and registration​

Ory Kratos now runs identity conflict lookups concurrently when checking whether an OIDC login or registration matches an existing account. Previously each credential identifier, verifiable address, and recovery address was checked one after another; now Kratos checks all candidates at once with bounded concurrency. This cuts sign-in latency, especially in multi-region deployments where each lookup crosses regions.


v26.3.1​

Address picker on the code refresh login screen​

Refresh (privileged re-authentication) logins that use the code method can now show a "Send code to

" button for each available address instead of asking for the identifier again. Because the active session already fixes the identity, re-entering the identifier is unnecessary and makes a privileged login look like a fresh login.

The behavior is controlled by a new feature flag, feature_flags.refresh_login_choose_address, which defaults to off. On Ory Network, set it through the project configuration field kratos_feature_flags_refresh_login_choose_address. New projects enable it by default; existing projects keep the previous identifier-input behavior until you turn it on. The flag is safe to toggle back and forth.

Device key IDs are now a deterministic fingerprint of the device key​

The client_key_id of a device authentication key is now derived by the server as the lowercase-hex SHA-256 of the enrolled device's public key. The enrollment request no longer accepts a client_key_id field; the id is always the fingerprint of the attested key.

  • The client_key_id field has been removed from the enrollment (add) request. Clients that still send it are not rejected: the field is ignored.
  • The id is bound to the key material that the platform attestation vouches for. At login, the server looks the key up by its stored fingerprint and verifies the assertion against the public key stored at enrollment time, so a login-provided id can no longer point the server at a different key.
  • The device recomputes the identical id locally from its own public key, so it is never returned by the server and can be used directly for login.
  • Because the id is a fingerprint of the key, the same physical device key can no longer be enrolled twice.

Existing keys keep their stored id and continue to work unchanged; the new derivation applies to keys enrolled from now on.

Fix device key enrollment on iOS​

Enrolling a device key from an iOS app now succeeds. Apple App Attest sends only the leaf certificate and the App Attest sub-CA in the attestation; the root is pinned on the server and never transmitted. The chain verifier previously treated the last certificate as the root and dropped the sub-CA, so a genuine two-certificate chain failed with "certificate signed by unknown authority". The verifier now treats every certificate except the leaf as an intermediate and anchors on the pinned root.

Pin device authentication keys to your own mobile apps​

Device authentication now supports optional App ID pinning, so a device key can only be enrolled from, and used to sign in from, an app you control.

Configure the allow-lists under the deviceauthn method:

selfservice:
methods:
deviceauthn:
config:
# Apple App IDs ("<TeamID>.<BundleID>").
ios_app_ids:
- ABCDE12345.com.example.app
# Android app signing-certificate digests (lowercase-hex SHA-256).
android_app_ids:
- 5f2e...c1a9

On iOS the App Attest rpIdHash (the SHA-256 of the App ID) is checked against ios_app_ids. On Android the attested AttestationApplicationId signing-certificate digest is checked against android_app_ids; the signing certificate is pinned rather than the package name, which an app cannot forge.

iOS pinning is enforced at both enrollment and login. Android pinning is enforced at enrollment only, because Android login is a bare signature that carries no attestation.

Pinning is opt-in: when a list is empty (the default), any Apple- or Google-attested app is accepted, so existing deployments are unchanged.

Reduce registration latency​

An improvement has been made to reduce latency of the Ory Kratos self-service registration API.


26.3.0

This version contains only minor changes and improvements such as dependency updates.


v26.2.22​

Support multiple passkey display-name traits in identity schemas​

Identity schemas can now flag more than one trait with passkey.display_name: true or webauthn.identifier: true. Creating a registration flow no longer fails with HTTP 500 and "no identifier found" when both email and phone (or any other combination) are flagged as passkey display-name sources.

The browser picks the first form field that the user actually filled in, so the same schema can serve email-only and phone-only signup paths without breaking either one. Schemas that flag a single trait, or none at all, continue to work as before.

The fix is backward compatible. Custom registration UIs that parse passkey_create_data and read the existing displayNameFieldName field continue to work; the new displayNameFieldNames array is published alongside it for clients that want the full candidate list.


26.2.21

This version contains only minor changes and improvements such as dependency updates.


v26.2.20​

Notify previous addresses on self-service email change​

Ory Kratos now supports notifying an identity's previous email address when they change their email address through self-service flows.

If the notify_previous_addresses hook is configured, the user will receive a notification at their previous email address informing them of the change. This notification includes information about the change and instructions on how to secure their account if they did not authorize the change.

This change includes a new email template for the notification, which can be customized to fit the branding and tone of your application. The template includes placeholders for the user's name, the new email address, and a link to secure their account if they did not authorize the change.


v26.2.19​

Stop forwarding the OIDC login_hint upstream parameter​

Kratos no longer forwards the login_hint upstream parameter to OIDC providers. A caller-supplied login_hint cannot be trusted, and on providers that act on it (such as Google, Microsoft, and Auth0) it can steer account, tenant, or OAuth client selection at the identity provider and enable a phishing client swap.

The hd, prompt, auth_type, and acr_values upstream parameters are unchanged and still forwarded.

Breaking changes​

login_hint is removed from the upstream_parameters of the login, registration, and settings OIDC methods. If you previously sent upstream_parameters.login_hint when submitting one of these flows, the value is now ignored and no longer reaches the upstream provider. The account chooser or sign-in form on the provider is no longer pre-filled from this value. No action is required; remove the parameter from your integration to avoid confusion.

Bind an identity to an organization through the settings flow​

The self-service settings flow now accepts an optional organization query parameter. Calling createBrowserSettingsFlow or createNativeSettingsFlow with an organization ID scopes the flow to that organization's SSO providers. The organization query parameter is only honored when one of the identity's email addresses is under the organization's configured domains; otherwise the flow falls back to a regular (unscoped) settings flow. The flow's UI returns only link nodes for the matching OIDC and SAML providers, with already-linked providers shown as disabled. Submitting a link runs the upstream IDP authentication and, on the callback, links the credential and sets identity.organization_id — no logout, no manual account linking.

The email-domain check uses the identity's email verifiable addresses. Identities whose schema has no verification.via: email annotation have no email verifiable addresses and therefore cannot use the organization query parameter — the flow silently falls back to an unscoped settings flow.

If the authenticated identity is already bound to an organization, that organization wins over the query parameter. Unlink submissions are rejected for organization-scoped flows. Link submissions are restricted to the flow's organization scope: an organization-scoped flow links only that organization's providers, and an unscoped flow links only providers that belong to no organization.

If the identity's organization_id references an organization that has since been deleted, the settings flow falls back to the unscoped path — accounts offboarded from a deleted organization keep working with account recovery and self-service settings.

Browser autofill and logout hardening for self-service flows​

Several self-service flows now emit the standard hints that browsers and password managers use to autofill credentials and one-time codes:

  • One-time code inputs in the login, registration, recovery, and verification code flows carry autocomplete="one-time-code", which lets browsers offer the code straight from the SMS or email and enables the Web OTP API on supported platforms.
  • The identifier input in the password login form carries autocomplete="username", so password managers offer the saved username. The identifier-first and passkey flows keep their existing username webauthn token.
  • SMS one-time code messages for login, registration, and verification now end with an origin-bound last line (@<domain> #<code>), matching the recovery code message. This binds the code to the requesting origin for the Web OTP API.

A new public endpoint, GET /.well-known/change-password, implements the W3C change-password URL. It responds with 303 See Other and redirects to the configured settings UI (selfservice.flows.settings.ui_url), so password managers can deep-link users to where they change their password.

Browser logout can now clear site data. When the new selfservice.flows.logout.clear_browser_data setting is enabled, a successful browser logout over HTTPS sends the Clear-Site-Data response header, instructing the browser to clear cookies, storage, and caches for the origin. The setting is off by default to avoid clearing data that other applications on the same origin may rely on.

Cursor-based pagination for SCIM​

The SCIM Users and Groups list endpoints now support cursor-based pagination as described in RFC 9865. Request the first page by sending an empty cursor query parameter (for example GET /Users?cursor&count=100), then follow the nextCursor value returned in each response to fetch the next page. Pagination ends when a response no longer includes nextCursor.

Cursor pagination keeps database load bounded no matter how deep you page, so it is the recommended way to read large numbers of users or groups — it has no equivalent of the startIndex offset limit. The existing count/startIndex offset pagination is unchanged and remains the default. Cursor responses omit totalResults.

Support for both methods is advertised under pagination in GET /ServiceProviderConfig. Cursors are valid for one hour; an expired or malformed cursor returns a 400 error with the expiredCursor or invalidCursor SCIM error type.

Filter SCIM users by group membership​

The SCIM API now supports filtering users by group membership. GET /Users?filter=groups.value eq "<group-id>" returns the direct members of the given group. The filter combines with both pagination modes — startIndex / count and cursor pagination — so members of large groups can be enumerated page by page:

GET /Users?filter=groups.value eq "47ac10b5-58cc-4372-a567-0e02b2c3d479"&cursor=...&count=100

Fix SCIM timeouts and conflicts on large group updates​

A membership change now re-evaluates the mapper only for the members that actually changed: the members added or removed, plus all current members when the group's display name changes.

Fix missing schemas and meta in SCIM ServiceProviderConfig​

The GET /ServiceProviderConfig response now includes the required schemas and meta attributes, as defined in RFC 7644. They were previously omitted because the response was serialized in a way that skipped the custom JSON encoder. SCIM clients that validate the resource against the SCIM schema now receive a compliant response.

Improved SCIM protocol conformance​

SCIM list and query responses now follow RFC 7643/7644 more closely:

  • count=0 returns only totalResults without fetching a page, also when combined with cursor pagination.
  • Empty result pages always render Resources: [] and include itemsPerPage.
  • The attributes and excludedAttributes query parameters support dotted sub-attribute paths such as name.givenName or members.value.
  • Group externalId filtering is now case-exact, matching user filtering.
  • PATCH operations normalize multi-valued attributes so at most one entry is primary, and patched resources are re-validated before they are saved.
  • Update conflicts return 409 with scimType: uniqueness only for genuine unique-attribute collisions; other failures return 500 so identity providers retry instead of giving up.

Kratos OEL now ships the full enterprise feature set​

Kratos OEL release adds enterprise features: organization OIDC sign-in, SCIM provisioning, FedCM, CAPTCHA, and device authentication.

On upgrade, Kratos OEL applies additional enterprise schema migrations. These are additive and run automatically as part of the standard migrate step.

Require a privileged session to change a recovery address​

Changing an identity's recovery address through the settings flow now requires a privileged (recently authenticated) session, matching the protection that already applied to credentials and verifiable addresses.

Previously, an identity using a recovery-only address schema could have its recovery address changed by a stale, non-privileged session without re-authentication. Such a change is now rejected and prompts the user to re-authenticate (session_refresh_required).

The default email-as-identifier schema was already protected and is unaffected.


v26.2.18​

Fix SCIM group membership data loss and conflict reporting​

Fixes several SCIM provisioning defects affecting Microsoft Entra ID, Okta, and WorkOS clients.

  • Removing a member from a group no longer removes that user from their other groups; the delete is scoped to the changed group.
  • Groups now return their complete membership: no duplicated entries, and no silent truncation past 1000 combined members and subgroups (which previously dropped the missing members on the next update).
  • PATCH remove without a path now returns 400 noTarget (RFC 7644) instead of erasing the entire user or group.
  • Creating a user that already exists returns 409 uniqueness instead of 500. Duplicate group externalId and cross-organization conflicts also use uniqueness.
  • Correct HTTP status on errors: an invalid filter on Users/Groups returns 400 invalidFilter (was 404 for Groups), and unexpected persistence errors return 500 (was 404).
  • An absent active attribute now defaults to active instead of creating a disabled user; explicit active: false still deactivates.
  • PATCH replace whose value-path filter matches nothing returns 400 noTarget instead of silently creating a fabricated element.

v26.2.17​

Make passkey authenticator, attestation, and timeout options configurable​

The passkey strategy now exposes advanced configuration under selfservice.methods.passkey.config that was previously hardcoded:

  • authenticator_selection.attachment, resident_key, and user_verification control which authenticators are eligible and whether biometrics or a PIN are required. attachment now defaults to no constraint, so by default users can register either platform or cross-platform authenticators. Set attachment: "platform" to restore the previous behavior of accepting only built-in authenticators.
  • attestation.preference sets the attestation conveyance preference sent to the authenticator.
  • timeouts.registration and timeouts.login set how long the WebAuthn ceremonies stay valid.

All options are optional. Defaults match the previous behavior, so existing deployments do not need to change anything. See the passkey documentation for a full reference and examples.

Reject DeviceAuthn enrollments using a revoked Android attestation CA​

Kratos now periodically fetches Google's Android attestation revocation list and rejects DeviceAuthn enrollments whose certificate chain contains a revoked CA.

The Android root CAs and the background goroutine that refreshes the revocation list are initialized lazily, on the first DeviceAuthn call and only when DeviceAuthn is enabled. Deployments that do not use DeviceAuthn pay no startup cost and emit no related logs.

Note: toggling selfservice.methods.deviceauthn.enabled from true to false at runtime does not stop the background goroutine; restart the process to fully disable it.

SCIM data mapper can read the existing identity​

The SCIM provisioning data mapper now receives the existing identity through a new identity external variable, alongside the existing scim variable. The variable exposes traits, metadata_public, and metadata_admin, each defaulting to an empty object when unset.

This lets a mapper preserve or merge attributes that SCIM does not manage. For example, an identity first created by a SAML or OIDC login may store fields such as an object ID in metadata_public. Because SCIM provisioning replaces metadata_public with the mapper output, those fields were previously overwritten. A mapper can now merge them back in:

local scim = std.extVar('scim');
local identity = std.extVar('identity');
local existingPublic = std.get(identity, 'metadata_public', {});

{
identity: {
metadata_public: existingPublic + {
// SCIM-managed fields. These win on key conflicts.
},
},
}

SCIM keeps full-replace semantics: the mapper output still replaces metadata_public unless the mapper deliberately merges the existing values back in. Mappers that do not reference the identity variable are unaffected.

SCIM enterprise user extension​

You can now provision enterprise user attributes through SCIM. Ory Network's SCIM endpoints support the urn:ietf:params:scim:schemas:extension:enterprise:2.0:User schema from RFC 7643 §4.3, including employeeNumber, costCenter, organization, division, department, and manager. Clients can read and write these attributes via POST, PUT, PATCH, and GET. PATCH paths and filter expressions accept the URN-prefixed form, so common provisioning flows (such as Okta's incremental manager updates) work without configuration changes. Filtering by employeeNumber is supported for indexed lookups.

A new /scim/{client}/v2/ResourceTypes discovery endpoint advertises the extension to clients that look at it.

The Jsonnet mapper for SCIM-to-identity transformation now sees the enterprise object under the URN key on the scim input, so customers can map enterprise attributes into identity traits or metadata.

SCIM provisioning now links an existing user into an organization when the user is not yet assigned to one and holds a verified email address whose domain matches one of the organization's configured domains. Previously, provisioning such a user returned a conflict.

When a SCIM client creates a user that already exists as an unclaimed identity, Ory checks the existing identity's verified email addresses against the SCIM client organization's domains. On a match, the existing identity is linked into the organization instead of failing with a conflict.

To protect against unauthorized access, Ory does not link a user when:

  • the matching email address is not verified,
  • the email domain does not match any of the organization's domains, or
  • the user already belongs to a different organization.

v26.2.16​

SCIM accepts schema-qualified attribute paths in PATCH and filters​

SCIM endpoints now accept attribute paths and filters that are qualified with a full schema URN, as some identity providers (for example Microsoft Entra) send.

Previously a PATCH operation whose path was qualified with a schema URN — for example urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager — failed with Could not decode request body. Those paths now parse and apply correctly. The same applies to filter query parameters on the Users and Groups list endpoints and to value-path filters inside a patch path.

A patch path or filter qualified with a schema the resource does not support is now rejected with a clear 400 error that names the schema. The schema comparison is case-insensitive.

SCIM request bodies and filter query parameters are now size-limited; an oversized body is rejected with 413 instead of being read in full, and an over-long filter with 400.


v26.2.15​

Block invisible-character duplicate accounts​

Hidden characters in an email or username — zero-width spaces, joiners, soft hyphens, the byte-order mark, and similar invisibles — are now removed before the identifier is stored. This stops someone from registering a second account that looks identical to an existing one but is treated as different. Identifiers are also case- and width-normalized, so ALICE@example.com and [email protected] are the same account.

International identifiers keep working: Hebrew, Arabic, and emoji are all accepted. Characters that merely look alike across scripts — such as the Cyrillic “а” and the Latin “a” — are kept distinct and do not collide.

Fix SCIM Group attribute filtering returning HTTP 500​

GET /scim/{client}/v2/Groups/{id}?attributes=... and GET /scim/{client}/v2/Groups?attributes=... now return the requested attributes correctly. Previously, any attributes= value on a Group endpoint caused a server-side panic that surfaced as HTTP 500 with an internal stack trace in the response body.

The excludedAttributes= form on Group endpoints, and all attribute filtering on User endpoints, were unaffected and continue to work as before.


v26.2.14​

Fix SCIM group memberships showing different results on /Groups and /Users​

For identities that belong to many SCIM groups, group changes could leave reads disagreeing with each other. GET /Groups/{id} listed the user as a member, but GET /Users/{id} did not — or vice versa.

This is fixed. Group membership changes on /scim/{client}/v2/Groups/{id} (PATCH, PUT, DELETE) now either fully succeed or fully fail. A failed request returns a clear error and leaves no partial state behind, so you can safely retry. Identities with hundreds of group memberships are also fully supported.

verify_new_address hook rejects duplicate email addresses​

The verify_new_address settings hook now checks whether the proposed new address is already owned by another identity before starting a verification flow.

Previously, a user could initiate an email change to an address that was already registered. The hook would send a verification code, and only at the point of code submission would the conflict surface. In the worst case — a race where another identity claimed the address between code generation and submission — the error was shown only on the verification page, with no way to recover without restarting the settings flow.

The hook now rejects the change immediately on settings submission if the address is taken, returning a duplicate credentials error on the settings page. If the race still occurs (address claimed after the hook runs but before the code is submitted), the duplicate credentials error is also shown on the verification page. Clicking "Resend code" after that error re-checks uniqueness and shows the same error rather than sending another code.


v26.2.13​

Detect mismatched database dialect at startup​

Kratos, Hydra, Keto, and backoffice now verify at startup that the configured database dialect matches the actual database server. The migrate sql and serve commands run a single SELECT version() query and refuse to start if the DSN scheme does not match the server.

Previously, pointing a service at a CockroachDB cluster with a postgres:// DSN (or the reverse) silently applied the wrong migrations and produced broken schemas. This is easy to do because CockroachDB speaks the PostgreSQL wire protocol and advertises a postgres:// DSN itself.

If your DSN scheme is wrong, you will now see a clear error such as:

DSN scheme postgres:// declares a PostgreSQL database but the server is
CockroachDB. Replace the scheme with cockroach:// so that the service
picks the correct migrations and SQL dialect.

To fix it, change the DSN scheme from postgres:// to cockroach:// (or the reverse) and restart. MySQL and SQLite are unaffected.

Extend Landlock sandbox to migrate and courier subcommands​

Kratos now applies the Landlock filesystem sandbox to the migrate sql and courier subcommands in addition to serve. After startup, these processes can only read and write the paths already allowlisted for serve: watched config files, TLS certificates, the courier template directory, SMTP client certificates, every file:// URI in the loaded configuration, the SQLite data directory (if SQLite is used), and any paths listed under security.landlock.allowed_paths.

This tightens the security posture of the migrate jobs and the courier worker pod without requiring any configuration changes. The existing security.landlock.disabled and security.landlock.allowed_paths settings continue to apply to all sandboxed subcommands. The sandbox is a no-op on kernels without Landlock support and is automatically skipped under go test.

Reject identity PATCH that desynchronizes a credential's type from its map key​

PATCH /admin/identities/{id} now returns 400 Bad Request when a JSON patch leaves a credential whose type field does not equal its map key — for example, replacing /credentials/password with a value that omits "type", replacing /credentials/password/type with an empty string, or adding /credentials/<newkey> with a value that omits "type".

Previously these patches were accepted with two different broken outcomes:

  • Replacing an existing credential subtree returned 200 OK but silently overwrote the credential's config with the patch value. For a password credential this destroyed hashed_password and prevented the user from logging in.
  • Adding a new credential key produced a misleading 500 Internal Server Error (No identity credential type "" could be found, this is a code bug.). The surrounding transaction rolled back, so the database itself was left intact in this case.

The new validation runs before any persistence, so neither outcome can occur. The check is enforced at two layers — the PATCH handler rejects the request directly, and IdentityManager.ValidateIdentity enforces the same invariant as a safeguard against any caller that bypasses the handler and writes to Identity.Credentials directly. The error response identifies the offending map key:

credentials.<key>.type must equal "<key>", got "<value>"

Valid PATCH operations on credential subtrees, such as /credentials/password/config/hashed_password, continue to work unchanged.

Reject invisible Unicode characters in identifiers to prevent duplicate accounts​

Identity identifier normalization now applies NFKC compatibility decomposition and strips Unicode format characters (category Cf) — zero-width spaces, soft hyphens, the byte-order mark, bidi controls, and other invisible code points. Previously, an attacker could register a second account with an email or username that was visually identical to an existing user's identifier but stored as a distinct string, bypassing the duplicate-account check.

After this change, [email protected] and alice​@example.com collapse to the same identifier and the second registration fails with a duplicate- credential error. The same fix covers the password, code, and webauthn login strategies, as well as recovery- and verification-address lookups.

B2B organization domain matching (MatchEmailDomain and the errorIfEmailAddressesAreClaimedByOrg hook) now canonicalizes both sides of the domain comparison the same way, so a Cf-poisoned domain in an organization's configuration no longer silently disables auto-enrollment for that domain's legitimate users.


v26.2.12​

Jsonnet worker is fully isolated from the filesystem​

The hidden jsonnet subcommand that Kratos re-execs to evaluate Jsonnet mappers (OIDC claim mappers, courier templates, identity-schema transforms) now applies an empty Landlock layer at startup. The worker only ever needs the inherited stdin/stdout/stderr — every path-based filesystem access from inside the worker is now denied by the kernel, even if a Jsonnet snippet were to bypass the in-process import barrier. Already-open file descriptors keep working, so the parent ↔ worker IPC is unaffected. This applies to Kratos OSS, Cloud, and OEL on Linux 5.13 and later, and is a no-op on other platforms.

This is not configurable, for simplicity and security: there is no legitimate use case to allow the Jsonnet VM to read from disk.

Landlock filesystem sandbox for kratos serve (https://docs.kernel.org/userspace-api/landlock.html)​

Filesystem sandbox for kratos serve (OEL)​

Kratos OEL activates the Landlock sandbox for the main kratos serve process after initialization. The process is then restricted to the files it needs at runtime:

  • /dev/null (subprocess plumbing)
  • /etc/resolv.conf and /etc/hosts (Go's pure-Go DNS resolver)
  • the running Kratos binary itself, with read+execute, so jsonnetsecure can re-exec it as a sandboxed worker
  • TLS certificates and keys for the public and admin listeners
  • the courier template directory (courier templates are loaded at runtime)
  • SMTP client certificate and key files (courier.smtp.client_cert_path, courier.smtp.client_key_path, and the equivalents under courier.channels[].smtp_config)
  • every file referenced from the config via a file:// URI. Kratos walks the loaded config once at startup and allows any value that begins with file:// — identity schemas, OIDC mapper templates, web_hook body templates, courier HTTP body templates, session tokenizer mappers and JWKS files, and anything added to the config schema later. Operators do not need to duplicate these paths under security.landlock.allowed_paths.
  • the directory containing the SQLite database — covers the database file itself and any -journal / -wal / -shm / transient -mj-XXXXX siblings
  • in the case of SQLite: /tmp, /var/tmp and /usr/tmp since they are needed by SQLite.
  • any paths listed in security.landlock.allowed_paths

The system trust store at /etc/ssl is not allowed. Operators who need to trust an additional CA must point SSL_CERT_FILE / SSL_CERT_DIR at the file and list it under security.landlock.allowed_paths. All other filesystem access is denied by the kernel after activation. This includes the /proc and /sys virtual filesystems.

The sandbox degrades gracefully on older kernels and non-Linux platforms, so no action is required for deployments that don't support Landlock. To opt out completely (not recommended in production), set security.landlock.disabled: true.

Configuration hot-reload still works as before with one caveat: Landlock restrictions are irrevocable for the lifetime of the OS process. Hot-reloading a config that flips security.landlock.disabled from false to true will not lift the sandbox — the process must be restarted for the change to take effect. Other config changes (allow list entries, courier templates, etc.) reload normally, but newly-introduced paths are only honoured on the next process start.

Symlinks in any configured path — --config files, TLS paths, identity.schemas[].url, the SQLite DSN, security.landlock.allowed_paths, and so on — are followed by the kernel when the rule is added at startup, so the rule attaches to the inode the symlink resolves to at that moment. As long as the target does not change, accesses through the symlink keep working transparently.

Landlock rules are irrevocable for the lifetime of the process. If a symlink target swaps at runtime — for example when cert-manager or certbot renews a certificate by writing a new file and re-pointing the symlink — the rule still references the original target inode. Accesses through the symlink then resolve to the new inode, which is not in the allowlist, and the kernel denies them with EPERM. Restart kratos serve after any such swap so the rules attach to the new inodes.

Troubleshooting​

A path that the sandbox does not allow surfaces in the application as EPERM ("Operation not permitted") on open(2) / openat(2) / execve(2) — Kratos typically logs it as permission denied while loading a config file, schema, template, or TLS material. To distinguish a Landlock denial from a regular Unix permission error, use the steps below.

  1. Confirm the sandbox is the cause. Check the Kratos startup logs for the line:

    level=info msg="Landlock filesystem sandbox is active."

    Just before it, two log lines list every path that was added to the allowlist:

    level=info msg="Landlock: collected roPaths." roPaths=[...]
    level=info msg="Landlock: collected rwDirs." rwDirs=[...]

    If the path that triggered EPERM is missing from both lists, Landlock is the cause. As a sanity check, restart with security.landlock.disabled: true: if the error disappears, the denial came from the sandbox.

  2. Read the kernel audit log. On Linux 6.10 and later, Landlock emits a kernel audit record for every denied access. The record names the syscall, the resolved path, and the denied access right. Read it with one of:

    sudo journalctl -k --since "5 minutes ago" | grep -i landlock
    sudo dmesg -T | grep -i landlock
    sudo ausearch -m LANDLOCK_DENY -ts recent # auditd-based distros

    A typical record looks like:

    audit: type=1334 audit(...): domain=2 op=fs blockers=fs.read_file path="/etc/kratos/schemas/fragments/address.json" dev="vda1" ino=131072

    The path= field is exactly what to add to security.landlock.allowed_paths. Older kernels (5.13 – 6.9) do not emit these records — fall back to step 3 there.

  3. Trace the syscall directly. When the audit log is unavailable or the path is templated, attach strace to the running process and watch for EPERM on the relevant syscalls:

    sudo strace -f -p "$(pgrep -f 'kratos serve')" -e trace=openat,execve -e status=failed

    Lines ending in = -1 EPERM (Operation not permitted) show the exact path the kernel rejected, even when Kratos's own log message has been swallowed by a wrapper.

  4. Fix the configuration. Once the offending path is known, add it to the allowlist (a directory grants every file underneath; a file grants only itself), then restart kratos serve — Landlock rules are immutable for the lifetime of the process, so a hot reload will not lift the denial.

    security:
    landlock:
    allowed_paths:
    - /etc/kratos/schemas/fragments/address.json

Normalize phone trait values to E.164 in webhook payloads​

When a phone trait is configured as a credential identifier (code strategy via sms), or as a recovery or verification address via sms, Kratos now rewrites the trait value to its E.164 form during validation.

Previously, only the credential identifier and the recovery and verification address tables stored the normalized value, while the traits JSON kept the raw user input. Webhook payloads templated against identity.traits.phone therefore saw a different value than the one Kratos used internally as the identifier. The trait is now consistent with the canonical form, so webhook consumers and Kratos see the same phone number.

Existing identities are not rewritten by this change. Use the kratos cleanup normalize-phones command to migrate stored identifiers; the trait values are corrected the next time the identity passes through validation (for example, on a settings or recovery flow that updates the trait).


v26.2.11​

Accept Microsoft Entra's non-standard SCIM payload for removing group members​

Microsoft Entra deviates from RFC 7644 when removing a single member from a group: instead of a value-path filter (path: members[value eq "<id>"]), it sends op: remove with a bare path: members and the member to remove in a value array. SCIM now interprets this payload as a selective remove of the listed members. Previously the bare path caused every member of the group to be cleared, so any Entra-provisioned group was emptied on a single-user remove event. RFC 7644-compliant payloads (a bare remove with no value, or a value-path filter) are unchanged.


v26.2.10​

Add unified bulk session management endpoint​

The admin session API now exposes a single endpoint for bulk disable and bulk delete operations across multiple identities or sessions:

  • POST /admin/sessions (manageSessions)

The action field selects the operation:

  • action: "disable" — soft-revoke matching sessions (sets active = false, preserves audit data).
  • action: "delete" — permanently delete matching sessions.

Targets are filtered by identities (a list of identity IDs) or sessions (a list of session IDs); exactly one of the two must be provided. To scope the operation to every session in the network, pass identities: ["*"]; the wildcard is not accepted in the sessions field and may not be mixed with explicit IDs. Up to 500 explicit IDs are accepted per call.

All requests respond 200 OK with {"processed": N, "more": <bool>}. processed reports how many rows the call affected (for disable, only sessions that were active before the call). more is true only when a wildcard request reached the per-call batch limit and additional matching rows may remain; callers should re-issue the same request to continue draining. Explicit-IDs requests always return more: false.

client.manageSessions({ action: "disable", identities: ["uuid-a", "uuid-b"] })
client.manageSessions({ action: "delete", sessions: ["uuid-c"] })
client.manageSessions({ action: "disable", identities: ["*"] })

No existing endpoints are changed.

Fix admin identity import for TOTP, lookup-secret, and passkey credentials​

This change fixes two related issues with the admin identity import API (POST /admin/identities and PUT /admin/identities/{id}).

TOTP and lookup-secret can now be used at AAL2 login after import. Previously, the import wrote the credential row but did not write the matching row in identity_credential_identifiers, so the AAL2 login flow could not resolve the credential and returned "You have no TOTP device set up." (4000015) for TOTP, or "You have not configured backup codes yet." for lookup-secret.

Passkey imports without a user_handle are now rejected with a 400. A passkey's identifier is the user_handle returned by the authenticator during the WebAuthn assertion. The userHandle is generated at registration time and stored on the authenticator, so it cannot be reconstructed by the server: the import has to supply it, or inherit it from a previously persisted passkey credential. Previously, an import that omitted user_handle would silently persist a non-functional credential. The import now rejects this case up front, and also rejects imports that supply an empty credentials.passkey.config.credentials list.

Reject imported password hashes with extreme cost parameters​

Kratos now bounds the cost parameters embedded in imported password hashes. Hashes that declare cost parameters far above any sane production value are rejected at the admin identity import API with a 400 Bad Request, and at login time the comparator returns a clear error instead of allocating gigabytes of memory or running for hours.

This closes a denial-of-service class where an attacker with write access to the identity graph could persist a hash whose decoded parameters would crash the Kratos process on every subsequent login attempt for that identity.

The bounds apply to Argon2 (m, t, p), PBKDF2 (i), plain scrypt (N, r, p), Firebase scrypt (ln, r, p), and bcrypt (cost). They are set comfortably above strong real-world configurations: Argon2 memory up to 1 GiB, PBKDF2 iterations up to 10,000,000, scrypt N up to 2^20, Firebase scrypt ln up to 17, and bcrypt cost up to 17.

Imports of correctly-configured hashes are unaffected.


v26.2.9​

Harden identity-schema loading against denial-of-service​

Adds a structural pre-parse gate to identity-schema loading. Before a customer-supplied schema reaches the upstream JSON Schema compiler, kratos now rejects schemas that exceed any of the following limits:

  • 1 MiB body size on the fetched schema document.
  • 32 levels of nested objects or arrays.
  • 1024 keys per object (properties, patternProperties, $defs, etc.).
  • 128 elements per array (allOf, anyOf, oneOf, tuple items, etc.).
  • 8192 total nodes across the schema tree.

The same pass also rejects $ref values that resolve to the document root (#, #/, or empty) — these produce a self-referential pointer in the compiled schema and would otherwise crash the kratos process via fatal stack overflow at validate time. Invalid regular expressions in pattern and patternProperties keys are now caught up front, instead of panicking deep in the upstream compiler.

When kratos fetches a schema URL outside of an HTTP request context (for example from a background task), the loader now attaches an SSRF-guarded HTTP client by default. Request paths that already attach a configured client via the request middleware are unaffected.

The Ory Network HTTP cache for identity schemas now caps response bodies at the same 1 MiB limit and refuses to cache anything larger.

Existing identity schemas at realistic sizes (well under all limits) are unaffected.

Patch security vulnerabilities in dependencies​

Bump several dependencies to patched versions to address security advisories reported by Dependabot.

Notable updates:

  • github.com/jackc/pgx/v5 to v5.9.2 across all Go modules (SQL injection via placeholder confusion in dollar-quoted string literals).
  • github.com/moby/spdystream to v0.5.1 (denial of service on container runtime interface).
  • go.opentelemetry.io/otel to v1.41.0 (remote DoS amplification via multi-value baggage header).
  • postcss to >=8.5.10 (XSS via unescaped </style> in CSS stringify output).
  • uuid to >=14.0.0 (missing buffer bounds check in v3/v5/v6 generators).
  • @xmldom/xmldom to >=0.8.13 (XML node injection and uncontrolled recursion).
  • axios, follow-redirects, lodash, picomatch, brace-expansion, serialize-javascript, yaml, file-type, i18next-fs-backend, @nestjs/core to their respective patched versions.

v26.2.8​

SSRF protection improvements​

Error messages originating from the SSRF protection mechanism no longer leak IP addresses if the hostname resolves to an internal IP address. This prevents SSRF recon through user-supplied URLs and hostnames.


v26.2.7​

Add security.disallow_ref_in_identity_schemas to lock down schema loading​

Introduces an opt-in config flag, security.disallow_ref_in_identity_schemas (default false), intended for multi-tenant deployments where identity-schema URLs come from untrusted operators.

When enabled, $ref URLs inside identity schemas may no longer resolve to file://, http://, or https://. This blocks server-side file reads and request forgery attempts via malicious identity schemas. Internal JSON-pointer refs (#/definitions/...) and self-contained base64:// refs remain allowed. Operator-configured top-level schema URLs are unaffected.

Ory Network forces the flag on. Existing self-hosted deployments keep their current behavior unless they explicitly opt in.

Support for required traits during OIDC on native applications​

On native applications, Kratos now supports asking the user to supply additional traits during the registration flow, if the traits are required by the identity schema, but not supplied by the OIDC mapper.

Additionally, you can use the updateRegistrationFlow to supply traits to be merged with the OIDC mapper data.

This aligns the functionality with the browser version of the registration flow.


v26.2.6​

Fix some incorrect WebAuthn error messages​

Some WebAuthn error messages were wrong: they mentioned TOTP instead of WebAuthn. They now have been fixed. This affect errors text and ID.

Correctly clear WebAuthn UI nodes on login error​

Due to a typo, WebAuthn UI nodes were not correctly cleared on login error, this has been fixed.

Breaking changes​

This is technically a breaking change if you relied on the exact wording of the error message, or on specific error IDs.

Add verify_new_address settings hook​

A new verify_new_address hook is available for the settings profile strategy. When configured, it defers email or phone number changes until the new address is verified. The identity's traits are only updated after the user completes verification of the new address.

This prevents immediate replacement of verified addresses and ensures the new address is reachable before applying the change.

Carry over upstream MFA to the Ory session​

Ory now trusts the authentication strength reported by upstream OpenID Connect providers. When a user signs in through an OIDC provider such as Auth0, Okta, Keycloak, PingFederate, Microsoft Entra v1, or any generic enterprise IdP, Ory reads the acr (Authentication Context Class Reference) and amr (Authentication Methods References) claims from the upstream ID token and records them on the resulting session.

You can now mark a provider's login as AAL2 based on those claims. Two new configuration fields on every OIDC provider let you specify which upstream values count as "the user already completed MFA":

  • aal2_acr_values — list of upstream acr claim values (for example urn:okta:loa:2fa:any, http://schemas.openid.net/pape/policies/2007/06/multi-factor) that elevate the session to AAL2.
  • aal2_amr_values — list of upstream amr claim values (mfa, otp, hwk, and other RFC 8176 values) that elevate the session to AAL2 when any of them appears in the upstream amr array.

If neither list matches, the session falls back to AAL1. If you enforce AAL2 through session.whoami_required_aal, Ory prompts the user for a second factor only when the upstream IdP did not already provide one. Both fields are optional; leave them empty to keep today's behavior.

The upstream acr and amr values are stored on the session's authentication_methods entry (upstream_acr and upstream_amr fields) for auditing. They are visible on /sessions/whoami and in webhook payloads.

You can configure the new fields for each provider in the Ory Console under Authentication → Social sign-in → Edit provider.

SCIM group events now include externalId and displayName​

SCIM group.created, group.updated, and group.deleted telemetry events now carry the group's externalId and displayName as attributes, alongside the existing group ID. This lets downstream consumers correlate events with the provisioning source of truth without a separate lookup.

SCIM group updates no longer rewrite unchanged members​

When a SCIM group update touches a field that does not affect its members (for example, patching the group's displayName to the same value), Kratos no longer rewrites the member identities. As a result, no IdentityUpdated event is emitted for members whose SCIM data did not change.

SCIMGroupUpdated events now include added and removed identity IDs​

SCIMGroupUpdated telemetry events now carry two additional attributes, SCIMGroupAddedIdentityIDs and SCIMGroupRemovedIdentityIDs, listing the identity IDs that were added to or removed from the group by the update. Downstream consumers can use this to act on the specific membership change instead of having to diff group state themselves.


v26.2.5​

Fix shared mutable state in error handling​

Error globals such as herodot.ErrNotFound were package-level variables shared across all requests. Calling methods like WithReason or WithDetail mutated these variables in place and returned the same pointer, so any request that added context to an error — reason text, details, etc, modified the global. The next request to reach an error path using the same error inherited those stale details.

As a consequence, observability (logs, traces) for requests resulting in an error suffered from the same issue: some errors were reported with details belonging to an unrelated request, or with fields missing that should have been present.

The new API creates a fresh error instance on each call, so each request gets its own copy.

The following values were at risk of leaking into unrelated error responses:

  • HTTP cookie names (Kratos CSRF flow)
  • Entity UUIDs (identity, organization, etc)
  • OAuth2 error hints (Hydra and Kratos Hydra bridge)
  • OIDC provider URLs and raw upstream error responses (Kratos OIDC strategy)
  • External schema fetch URLs and HTTP status codes (Kratos schema handler)
  • JWT claims and issuers (Oathkeeper JWT authenticator)

No data was written to persistent storage or transmitted outside the error response. Any two requests hitting the same error path on the same node — even back-to-back with no concurrency — could exchange error details.

Under concurrent load, the shared writes also constitute a true data race, which can additionally produce errors in an inconsistent or partially written state.

This change has no externally observable effect other than fixing the information leak in error paths.

Native OIDC registration now returns the flow ID when required traits are missing​

When a native or API-based OIDC registration flow encounters a validation error because of missing required identity traits, the return_to redirect now includes the flow query parameter alongside the existing code parameter.

This allows native clients to fetch the registration flow, identify which fields are missing, and re-submit with complete data. Previously, only the code parameter was included, leaving native clients with no way to recover from missing traits during social sign-in registration.

Browser flows were not affected by this issue.

Phone numbers are now normalized to E.164 format​

Kratos now normalizes phone numbers to E.164 format when used as identifiers, verifiable addresses, or recovery addresses. This ensures consistent storage and lookup regardless of how a user enters their phone number (with spaces, dashes, or parentheses).

Existing identities with non-normalized phone numbers continue to work. A new CLI command kratos migrate normalize-phone-numbers is available to normalize legacy phone data in the database. Run this command after deploying the update to ensure all phone numbers are in E.164 format.

Render identity schema enum traits as dropdowns​

Identity schema properties that declare an enum are now surfaced to the Account Experience and rendered as native <select> inputs, so users can pick from the allowed values instead of typing them into a free-form text box.

Kratos attaches the enum values to the UI node as an options array on InputAttributes. When present, the Account Experience falls back to rendering the field as a dropdown; consumers that do not know about options continue to render a text input as before, so the change is backward compatible.


v26.2.4​

Fix settings flow requiring reauthentication when editing non-privileged traits on verified identities​

Users with a verified email address can now update non-privileged traits (such as first name or last name) through the settings flow without being prompted to reauthenticate. Previously, the privileged-session check incorrectly triggered whenever any verifiable address on the identity had a non-nil verification timestamp, because the internal deep copy of the identity corrupted the timestamp's representation.

Fixes ory/kratos#4435.

Improve the performance of finding identity credentials​

Kratos now loads identity credential type mappings from the database once at startup and caches them in memory for the lifetime of the process. This eliminates a JOIN on the identity_credential_types table from every call to FindByCredentialsIdentifier, reducing query cost and improving login performance.

This also works for self-hosted installations that have non-standard UUIDs in the identity_credential_types table, from a time where these UUIDs were dynamic (they now are constants).

Include metadata_admin in admin session endpoint responses​

Admin session endpoints now include metadata_admin in the expanded identity, consistent with admin identity endpoints. Previously, metadata_admin was stripped from session responses even on admin endpoints.

Affected endpoints:

  • GET /admin/sessions?expand=identity
  • GET /admin/sessions/{id}?expand=Identity
  • GET /admin/identities/{id}/sessions

Public endpoints remain unchanged and continue to exclude metadata_admin.

Migrate Helm chart repository URL from k8s.ory.sh to k8s.ory.com​

The Helm chart repository URL has been updated from k8s.ory.sh to k8s.ory.com. The old URL will continue to work with redirects for a limited time. Update your Helm repository configuration to use the new URL.

Support importing all credential types when updating an identity​

The PUT /admin/identities/{id} and PATCH /admin/identities endpoints now support importing credentials for all credential types, consistent with what is already available when creating an identity.

You can now import the following credential types via the update endpoint:

  • totp: Replaces existing TOTP credentials.
  • lookup_secret: Adds new lookup secret codes to the existing set.
  • webauthn: Preserves existing credentials, adds new ones, and updates credentials with matching IDs.
  • passkey: Preserves existing credentials, adds new ones, and updates credentials with matching IDs. New user handles are appended to the identifiers list.
  • oidc, saml: Keeps existing credentials and adds the new ones to the list.

v26.2.3​

Remove CGO requirement from Ory by switching to modernc.org/sqlite​

Migrates SQLite support from mattn/go-sqlite3 (CGO) to modernc.org/sqlite (pure Go) and fixes several timestamp comparison bugs that caused incorrect pagination results. This migration is fully backwards compatible. Going forward, the -tags sqlite tag is no longer required to compile Ory.


v26.2.2​

Phone number verification with SMS code​

You can now verify phone numbers through the standalone verification flow, not just as a side effect of registration. When an SMS courier channel is configured, the verification page accepts both email addresses and phone numbers.

  • The verification form input label changes to "Email or phone number" when an SMS channel is configured. Without an SMS channel, the label remains "Email".
  • Submitting a phone number sends a verification code via SMS.
  • The UI shows SMS-specific confirmation and success messages instead of email-specific ones.
  • Unknown phone numbers are handled silently (no SMS is sent) to prevent enumeration and avoid SMS costs.

Update identity traits and metadata from OIDC claims on login​

You can now configure OIDC providers to automatically update identity traits and metadata from the upstream provider's claims on every login — not just during registration.

Set update_identity_on_login to automatic on any OIDC provider to enable this. The same Jsonnet claims mapper that runs during registration will re-run on each login. If the mapper output differs from the stored identity, traits and metadata are updated before post-login hooks run.

The mapper receives the current identity as std.extVar('identity') with traits, metadata_public, and metadata_admin, so you can write conditional logic that preserves existing values or merges selectively.

When the mapper omits metadata_public or metadata_admin from its output, existing values are preserved. When it explicitly outputs {}, the fields are cleared.

This setting is available per provider in the Ory Console under advanced settings, and via the API as update_identity_on_login on the provider configuration.


v26.2.1​

Fix identity disabled error handling during self-service flows​

Previously, when attempting to recover or login as an identity that had been disabled (state set to "inactive"), Ory Kratos would return a generic unauthorized error for API and SPA clients.

Instead, Ory Kratos now adds a newly introduced UI message (with ID 4010011) to the flow's UI messages indicating the identity is disabled.

Breaking changes​

For API and SPA clients, the error response when interacting with recovery and login flows for disabled identities has been changed from a generic unauthorized error to a specific error indicating that the identity is disabled. This allows clients to handle this case more gracefully.

Upgrade instructions:

  • If your application already handles "invalid credentials", and other similar errors, no change will be required.

  • If your application specifically checks for the previous generic unauthorized error during recovery or login flows, you will need to update your error handling logic to check for the new specific "identity disabled" error message in the UI messages of the respective flow objects.


v26.2.0​

Surface external ID conflicts in batch identity import​

A bug was fixed that caused conflict errors on the external_id field to be hidden during batch identity imports.

This issue occurred when multiple records with the same external_id or records with an external_id that already existed in the database were imported. This made it impossible to identify the source of the conflict.

With this fix, any conflicts on the external_id field will now be properly reported, allowing for easier troubleshooting and resolution of import issues.


26.1.18

This version contains only minor changes and improvements such as dependency updates.


v26.1.17​

Add UAE PASS as a social sign-in provider​

Adds support for UAE PASS authentication, the UAE's official digital identity platform. Supports both web and mobile authentication flows.

For setup instructions, see the Ory UAE PASS guide.

Native mobile apps and other API clients can now manage OIDC (social login) connections through the settings flow. Previously, link and unlink nodes were only populated for browser flows. The link action returns a 422 browser_location_change_required error with a redirect URL, following the same pattern used by login and registration flows.


26.1.16

This version contains only minor changes and improvements such as dependency updates.


26.1.15

This version contains only minor changes and improvements such as dependency updates.


26.1.14

This version contains only minor changes and improvements such as dependency updates.


v26.1.13​

Add CAPTCHA support to recovery and verification flows​

Extends CAPTCHA widget integration to include the self-service verification and recovery flows. This aligns security capabilities with existing login and registration implementations.

Add URL scheme validation for callback and redirect URLs​

In Kratos' callback and redirect URLs (...default_browser_return_url), we now only allow for http:// and https://, or empty schemes.

Passkey support for API flows​

Kratos now supports passkey registration, login, and settings management through API flows. This enables passkey authentication in mobile apps through native frameworks such as React Native, Swift, and Kotlin.


26.1.12

This version contains only minor changes and improvements such as dependency updates.


v26.1.11​

Breaking changes​

Upgrading from <26.1.7 will break pagination tokens. To ensure smooth upgrade, you must upgrade to 26.1.7 first.


26.1.10

This version contains only minor changes and improvements such as dependency updates.


26.1.9

This version contains only minor changes and improvements such as dependency updates.


26.1.8

This version contains only minor changes and improvements such as dependency updates.


26.1.7

This version contains only minor changes and improvements such as dependency updates.


v26.1.6​

Proper validation message for invalid email addresses and phone numbers​

Kratos' APIs will now return more user-friendly validation messages when users provide invalid email addresses or phone numbers during registration or profile updates.

Breaking changes​

The message IDs for invalid email addresses and phone numbers have changed. Clients should update their localization files accordingly:

  • Invalid email address:
    • Old ID: 4000001 (with generic "value" is not valid "email" message)
    • New ID: 4000040 (with user-friendly "Enter a valid email address" message)
  • Invalid phone number:
    • Old ID: 4000001 (with generic "value" is not valid "phone" message)
    • New ID: 4000041 (with user-friendly "Enter a valid phone number" message)

26.1.5

This version contains only minor changes and improvements such as dependency updates.


26.1.4

This version contains only minor changes and improvements such as dependency updates.


26.1.3

This version contains only minor changes and improvements such as dependency updates.


v26.1.2​

Properly accept OAuth2 login challenges in account linking flows​

In the account linking flows, the OAuth2 login challenge was not properly accepted and stored in the login flow. This meant that users going through account linking would not be redirected back to the OAuth2 client after logging in, but had to re-confirm their credentials a second time. This has now been fixed by properly accepting and storing the OAuth2 login challenge in the login flow.

Properly fetch OAuth2 login challenge during code flows​

During the one time code registration & login flows in Kratos, if the flow was initiated from Ory Hydra, the OAuth2 login challenge was not properly fetched and returned in all API calls. This issue was resolved, ensuring that the OAuth2 login challenge is now correctly retrieved and included in the responses during these flows.


v26.1.1​

Fix null response in OAuth2 login flow with existing session​

Fixed a bug where the /self-service/login/browser endpoint would return null instead of a proper error response when called with an existing session, a Hydra login challenge, and Accept: application/json header.

Requests to Admin Endpoint without /admin Prefix No Longer Redirect​

The admin endpoint now serves requests that omit the /admin prefix instead of redirecting them with the correct path. This change is not expected to affect existing clients, but instead avoids unnecessary redirects.


v26.1.0​

Accept Hydra Login Challenge in Verification Flows​

Fixes a bug where Kratos would not accept the Hydra login challenge in verification flows, if they originated from a refresh login flow.

This would manifest as an "unknown form state" in Ory Elements and the Ory Account Experience.

Change in Event Attributes​

Login and Registration Events now include SelfServiceMethodUsed attribute. The SelfServiceStrategyUsed attribute is deprecated, and it is advised to use newly introduced SelfServiceFlowName attribute.


25.4.12

This version contains only minor changes and improvements such as dependency updates.


25.4.11

This version contains only minor changes and improvements such as dependency updates.


25.4.10

This version contains only minor changes and improvements such as dependency updates.


25.4.9

This version contains only minor changes and improvements such as dependency updates.


25.4.8

This version contains only minor changes and improvements such as dependency updates.


25.4.7

This version contains only minor changes and improvements such as dependency updates.


25.4.6

This version contains only minor changes and improvements such as dependency updates.


25.4.5

This version contains only minor changes and improvements such as dependency updates.


25.4.4

This version contains only minor changes and improvements such as dependency updates.


25.4.3

This version contains only minor changes and improvements such as dependency updates.


v25.4.2​

Returning oauth2 login challenge on flow updates​

Fixed a bug where the oauth2 login challenge was not returned when updating a login or registration flow. This ensures that clients receive the necessary information to proceed with the authentication process.


v25.4.1​

Session is now included in all settings hooks payloads​

When configuring a settings flow hook in Ory Kratos, the session is now included in the payload sent to the hook. This allows for better context and handling of user sessions during settings flows.

Streamlined one-time code login flow​

This update removes an entire step from passwordless and 2-factor login flows when a one-time code is the only available authentication method.

Previously, users entered their identifier and then had to manually request a login code. With this change, the code is now sent automatically as soon as the identifier is submitted.

By removing this extra step, the login experience becomes faster, simpler, and more intuitive for users who rely exclusively on one-time codes.


v25.4.0​

Session now available in settings after hook​

When using the After Hook feature in Ory Kratos, the session is now accessible in the webhook's body context. This enhancement allows developers to access session information directly within the after hook, enabling more dynamic and context-aware settings webhooks.

Significantly faster identity updates​

This patch improves the performance of identity updates by optimizing database queries and reducing redundant operations. Users will experience faster response times when signing up, updating their profiles, or changing credentials.


v25.3.9​

Make the ID of the recovery address of an identity optional when creating it​

Before this fix, attempting to create, via the API, an identity with a recovery address which did not contain an ID, would fail with an opaque error (uuid: incorrect UUID length 0 in string ""). Now, if the ID is not present at creation time, it will be auto-generated.


v25.3.8​

Add 'Login with Amazon'​

Amazon is added to the list of OIDC providers to enable 'Login with Amazon'.

Note that 'Code Based Linking' and 'Login and Pay with Amazon' are not part of this release.


25.3.7

This version contains only minor changes and improvements such as dependency updates.


25.3.6

This version contains only minor changes and improvements such as dependency updates.


v25.3.5​

Change of base image​

The base image for OEL images is now set to "gcr.io/distroless/static-debian12:nonroot". Previously, it was "gcr.io/distroless/static-debian12:debug-nonroot", which included BusyBox (a minimal shell and basic debugging utilities). Debug images are still available using the "-debug" tag suffix.


v25.3.4​

Remove counting courier messages in /admin/courier/messages​

We've removed the X-Total-Count HTTP response header from the /admin/courier/messages endpoint. This change prevents a costly, linear scan of the database on every request, which was defeating the purpose of pagination.

Additionally, there is a new configuration field in Kratos: secrets.pagination which is a list of encryption keys used to encrypt/decrypt the pagination token. This is not security sensitive and only used to avoid having API clients relying on the specific implementation of the pagination token. This way the implementation is free to change in the future. If no keys are configured, a fallback key is transparently used.

Breaking changes​

  • Clients can no longer rely on the X-Total-Count HTTP response header.
  • The total number of messages is now indeterminate. You can determine the last page when the number of returned items is less than the page size. But note that due to the real-time nature of this data, subsequent calls with identical filters may return different results as the total number of messages can change rapidly.

v25.3.3​

FedCM Sign-In Now Triggers Login Hooks​

We updated the Federated Credential Management (FedCM) sign-in flow to correctly trigger any "after login" hooks configured for the OIDC (OpenID Connect) method.

This change ensures that when a returning user signs in using a FedCM provider, the system behaves consistently with other OIDC-based logins.

Improved tracing and metrics for the high-performance SQL connection pool​

This change adds distributed tracing and advanced metrics for the high-performance SQL connection pool in all Ory OEL products.


2025-07-25​

Tag: beebb63c5cd4a58b218a792027b34d231735dc05

Support for External Identity IDs​

Identity admins can now set an external_id field on an identity. If set, the external ID must be unique within the project. Admins can retrieve an identity by external ID through the /admin/identities/by/external/{id} endpoint. In responses, the external_id is contained in all identity and session responses. When tokenizing a session, the external_id is available in the Jsonnet context. Optionally, admins can set the subject_source option on the tokenization config to external_id, in which case the sub claim will be the external ID. Note that in this case tokenization fails if the external ID is not set for the identity bound to that session.

Read more about the external ID feature in our documentation: https://www.ory.com/docs/kratos/manage-identities/external-id

info

This release needs previous version (097934fff2bda05c808d962a92f52140f80dff83) to be applied first

Tag: 097934fff2bda05c808d962a92f52140f80dff83

Improved SQL queries to gracefully handle new columns added via future migrations, preventing upgrade-time failures due to schema changes.

2025-06-11​

Tag: 290abca8469dc46c1ba07708849fed28fdbc1b69

Make code submission attempt limit configurable​

Previously the maximum number of submission attempts for codes (e.g., 2FA codes, email verification codes) was set to 5 in Ory Kratos. This release allows the submission attempts to be configurable for system administrators based on specific security policies or business requirements, the default value of 5 remains. To find out more how to configure it please read our documentation https://www.ory.com/docs/identities/sign-in/code-submission-limit

2025-05-27​

Initial Ory Kratos OEL release​

Changes:

  • fix high security vulnerability CVE-2025-22871

No specific upgrade steps are required for this release. The Ory Kratos OEL image is now available in the Ory Enterprise Docker Registry.