Skip to main content

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.

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.