- RP-initiated logout. A relying party sends the browser to your
end_session_endpoint. You end the provider session and redirect back. - Back-channel logout. When a provider session ends for any reason, theAuth posts a signed logout token to each relying party’s
backchannel_logout_uri, server to server.
What turning it on changes
Passlogout to createOidcProviderModule. Then:
- Discovery gains
end_session_endpoint,backchannel_logout_supportedandbackchannel_logout_session_supported. - ID tokens carry a
sidclaim. provider.logoutexists, and clients can be registered withpostLogoutRedirectUrisandbackchannelLogoutUri.- Each authorization is recorded in a sid index (
theauth_oidc_sessions) so a later logout knows which relying parties to notify. Client logout settings live intheauth_oidc_logout_clients.
Wire it up
There is a loop to untangle. The provider needs a session manager and database, and the session manager’sonRevoke needs the provider. A late-bound reference breaks it:
oidcLogout(provider) mounts GET and POST at logout.endSessionPath (rate limited to 30 per minute). It throws at startup if the provider was created without logout.
Because onRevoke is on the session config, every revoke fans out: a user signing out, revokeAll after a password reset, or an eviction by maxConcurrent.
The route is registered under your adapter prefix, but discovery advertises
${issuer}${endSessionPath}. If you mount theAuth at /api/auth, either serve the endpoint at the issuer root with a rewrite or set endSessionPath to match, so the advertised URL resolves.Link the session when you authorize
Pass the provider session’s id toauthorize so the authorization is tied to it:
sid is derived from the session id (deriveSid(issuer, sessionId)), so revoking that session finds every relying party that holds it. Without sessionId the sid is random. Logout still works when the relying party supplies id_token_hint or you end all sessions for a user, but revoking a specific session cannot find the authorization.
Register a client
config.logout fails with LOGOUT_NOT_ENABLED. Post-logout URIs are an exact-match allowlist. The back-channel URI must be https unless allowInsecureBackchannel is on. To change them later, call provider.logout.setClientLogoutConfig(clientId, { postLogoutRedirectUris, backchannelLogoutUri }).
The end session endpoint
The relying party redirects the browser toend_session_endpoint with:
It revokes the session named by the request cookie (only if it belongs to the hinted user) and any session the
sid maps to, notifies relying parties, runs onEndSession, then answers 302 to the post-logout URI, else defaultRedirectUri, else 200.
Back-channel fan-out
For each relying party with a back-channel URI, theAuth signs a logout token with the provider key and posts it asapplication/x-www-form-urlencoded (logout_token=...). The token has iss, aud (the client id), sub, sid, jti, iat and the back-channel logout event, and no nonce.
Delivery refuses non-https URLs and hosts that resolve to private addresses, never follows redirects, and times out after backchannelTimeoutMs. One token goes to each client per sid and user, in parallel.
You can also trigger it directly. Both return { attempted, delivered, failed } and never throw:
Limits
- There is no retry. The sid index rows are claimed (deleted) before delivery. A relying party that is down, times out or returns an error does not get the token again. It shows up in
failedand nowhere else, so log it if you care. The relying party’s own session then lives until it expires. Pair back-channel logout with short relying-party sessions. - Delivery runs inline.
onRevokeis awaited, so a slow relying party delays the revoke that triggered it by up to the timeout. - Index rows expire. After
indexTtlSecondsan old authorization can no longer be found, and ending that session notifies nobody. - Local logout always completes. A fan-out failure is swallowed, never blocks revocation.
Receive logout tokens (relying party)
An app that signs users in with an OIDC provider, including another theAuth, can accept back-channel logout withcreateBackChannelLogoutReceiver:
sid on the local session when the user signs in, so a sid can be matched:
Customize with
sessionRevoker: { resolveUserId, findSessionsBySid, sidMetadataKey }, or pass your own revokeSessions(claims). onLogout(claims) still runs after revocation if you set it. The endpoint never answers 200 when nothing is configured to sign the user out.
The receiver checks issuer, audience, signature (RS256, ES256 or EdDSA by default), that the token is at most 120 seconds old (maxAgeSeconds), the back-channel event, no nonce, and a jti it has not seen. The default replay store is per process, so give every instance a shared replayStore (markSeen(jti, ttlSeconds)) if you run more than one.
If you use stateless sessions instead, see Session hardening for pairing the receiver with isRevoked.
Related
OIDC provider
Run theAuth as an OpenID Connect provider.
Sign-in gate
Session limits and the onRevoke hook.
Session hardening
Stateless sessions and federated logout.