v26.3.7
v26.3.7
Require client authentication when exchanging IdP-initiated authorization codes
Polis now requires client authentication on the OAuth token endpoint for authorization codes issued by IdP-initiated SSO flows. A
token request must supply a valid client_secret. Requests that omit all credentials are rejected with
401 Please specify client_secret or code_verifier.
Previously, authorization codes from IdP-initiated flows had no associated session, and every client-authentication branch on the
token endpoint was guarded by that session. A token request that omitted client_id and client_secret skipped all checks and
was issued an access token. Anyone who intercepted such a code could exchange it without knowing the application's client secret.
SP-initiated flows were not affected. Integrations that already send client credentials continue to work unchanged.
Breaking change. Any integration that exchanges an IdP-initiated authorization code without a client_secret (relying on
the previous behavior) now receives 401. To restore access, send client_secret on the token request. If you do not use
per-connection secrets, send the deployment's CLIENT_SECRET_VERIFIER value (which defaults to dummy when unset).
Bind Google Directory Sync OAuth callback to a single-use state
The Google Directory Sync OAuth flow now uses an unguessable, single-use state handle that is stored server-side and bound to
the directory the flow was started for. The callback resolves the directory from that stored record and deletes it on use, instead
of trusting a directoryId supplied in the state query parameter.
Previously the state carried the directoryId with no binding, so a crafted or replayed callback could install one tenant's
Google Workspace tokens onto another tenant's directory (CWE-352). Callbacks with a missing, unknown, or already-used state are
now rejected. Any authorization flow started before the upgrade must be restarted from the directory's authorization URL.
Clean up partially initialized controllers when startup fails
If constructing the Polis controllers fails after the database has been created -- for example when a later initialization step
throws -- controllers() now closes the database and stops the background workers it already started before rethrowing, instead
of leaving their timers and connection pools on the event loop with no close() for the caller to invoke. The database wrapper's
close() now also always clears its stats-reporting interval, even if the underlying driver close rejects.
This affects self-hosted Polis.
Fix SCIM authentication bypass on non-SCIM directories
Public SCIM endpoints now reject requests to directories that do not use SCIM (such as Google), and reject any request that presents an empty or missing bearer token. Previously, directory types that store an empty SCIM secret were authenticated by a request that also carried no token, because a constant-time comparison of two empty values returns true. An attacker who knew a directory id could read and write the tenant's SCIM users and groups without credentials. This affected the self-hosted and OEL distributions; the endpoint is not exposed on the Ory Network.
Fixed a security issue
This release includes a fix for a security issue. Additional details will be shared with customers directly.
Release background timers on shutdown and await webhook batch processing
Embedding Polis and calling the controllers' close() now stops every background timer it started, so the host process can shut
down cleanly. Previously close() left the SSO traces daily cleanup timer and the in-memory store's TTL cleanup timer running,
which kept the Node.js event loop alive after shutdown and forced callers to terminate the process explicitly.
The webhook batch processor now waits for a batch to finish before it returns. EventProcessor.process() previously started
processing without awaiting it, so its error handling never ran and the "already running" guard could clear before a batch
completed, allowing overlapping runs. It now awaits the batch, logs any failure through the configured logger, and holds the run
guard for the full duration.
This affects self-hosted Polis.
SAML IdP Single Logout (SLO) for Identity Federation
Polis can now act as a SAML IdP for Single Logout. When a Service Provider sends a SAML LogoutRequest, Polis initiates an OIDC RP-Initiated Logout against the upstream OIDC provider and returns a signed SAML LogoutResponse to the SP. This mirrors the existing login flow.
SAML responses are protected against assertion replay
Jackson now rejects a SAML response whose assertion has already been used. Each consumed assertion is recorded until it expires, so a captured response can no longer be submitted more than once to obtain a fresh login.
This closes a gap in the IdP-initiated flow. Those responses carry no in-flight request to consume, so previously the same signed response could be replayed within its validity window to mint a new authorization code for the victim on each submission. Service-provider-initiated flows were already protected because their one-time request state is consumed on success.
The anti-replay binding (InResponseTo) is now read only from the signed part of the assertion, so it can no longer be bypassed
by editing the unsigned response wrapper.
Single Logout responses now verify the Identity Provider signature before acting on the response status or request ID, so an unsigned or tampered logout response is rejected outright.
Setup links are now scoped to their service
A setup link is now rejected with 403 when it is used for a service it was not issued for. A Single Sign-On setup link can only
manage SSO connections, and a Directory Sync setup link can only manage directories.
Before, the service on a setup link was not checked at the setup routes, so the holder of a Directory Sync link could create SSO connections for the tenant (and the reverse), which is an authorization bypass. The SSO connection route also no longer accepts the tenant, product, or redirect URLs from the request body; it takes them only from the setup link.