Back-Channel Logout
OpenID Connect Back-Channel Logout 1.0 (Back-Channel Logout) lets the OpenID Provider tell a relying party (your application) that a user's session has ended, directly from server to server. ThunderID implements it as the sender. When a Single Sign-On session ends, ThunderID posts a signed logout token to every application that took part in the session and registered a back-channel logout URI.
RP-Initiated Logout ends the session at ThunderID. It does not clear what each application remembers about the user. Back-channel logout closes that gap: every participating application hears about the termination and can end its own session, including applications that did not start the sign-out, and including sessions that an administrator ended or that ended because the user was deleted. The message does not depend on the browser, so it arrives when the browser is closed, when frames are blocked, and when the user is on another device.
How It Works
- The user signs out of Application A.
- Application A sends the browser to the end session endpoint of ThunderID (RP-Initiated Logout).
- ThunderID runs the sign-out flow, then ends the SSO session and revokes the tokens issued in it, in one transaction.
- ThunderID redirects the browser to the
post_logout_redirect_uri. It does not wait for any delivery first. - After the transaction commits, ThunderID generates one logout token per participating application that registered a back-channel logout URI, and posts it to that URI in the background.
- Application B validates the token and ends the local session the token names.
- Application B answers
200. A delivery that fails with a retryable response is tried again with a fresh token. A delivery that is rejected is recorded and not retried.
RP-Initiated Logout is one trigger. A sign-out flow run through the Flow Execution API, and an administrative action such as revoking a user's sessions or deleting the user, end the session in the same way and send the same notifications from step 5 on.
How ThunderID Implements It
| Aspect | Behavior |
|---|---|
| Trigger | Every path that terminates an SSO session: RP-Initiated Logout, a sign-out flow, and subject-wide revocation such as user deletion. Session expiry does not send a notification |
| Who is notified | Every application that joined the session and has a backchannelLogoutUri. An application without one is skipped |
| Ordering | ThunderID does not wait for deliveries before redirecting the browser after RP-Initiated Logout |
| Discovery | Advertised as backchannel_logout_supported and backchannel_logout_session_supported, both true while the feature is enabled, see Server Metadata |
| Encrypted tokens | A client configured for ID token encryption receives an encrypted logout token, with iss replicated in the JWE header |
| Observability | One BACKCHANNEL_LOGOUT_DELIVERED or BACKCHANNEL_LOGOUT_FAILED event per participant, carrying the client id, the session id, the attempt number and the HTTP status. The token, the subject and the request body are never recorded |
Register a Back-Channel Logout URI
Set the URI on the application's OAuth configuration. In the Console, open the application, go to Advanced → OAuth 2 Configuration, and set Back-Channel Logout URI. Through the /applications API or a declarative configuration, the field is backchannelLogoutUri; Dynamic Client Registration uses backchannel_logout_uri. An agent that is an OAuth client carries the same backchannelLogoutUri field through the /agents API, and is notified like an application when a person signs in through it.
backchannelLogoutUri: https://app.example.com/backchannel-logout
| Rule | Behavior |
|---|---|
| Empty | The application is not notified |
| Format | An absolute URI with a scheme and a host. No user credentials, no fragment, no * |
| Scheme | https, or http for a confidential client only. A public client must register an https URI |
| Host | localhost, *.localhost and loopback, link-local, private or unspecified IP literals are rejected by default. Set reject_private_addresses to false for relying parties on a private network, see Configuration |
Use https for any endpoint outside a private network. Plain http exists for confidential clients on an internal network and for local development.
The whole OAuth configuration is validated on every write, so a change elsewhere in it, such as making a confidential client public, can make a previously accepted http URI invalid.
SDK Support
The ThunderID SDKs that run on a server implement the receiving side. Each validates the token as described in Validate the Token, ends the local session it names, and answers with the status codes in Respond. The Express, Node, Next.js and Nuxt SDKs do not decrypt a JWE (step 2), so they reject the encrypted logout token a client configured for ID token encryption receives.
| SDK | What to add | Reference |
|---|---|---|
| Express | app.post('/backchannel-logout', handleBackchannelLogout()) | handleBackchannelLogout() |
| Node | client.handleBackchannelLogout(logoutToken) behind a route of the application's own | ThunderIDNodeClient |
| Next.js | export const {POST} = backchannelLogout() in a route handler | backchannelLogout() |
| Nuxt | thunderid: {backchannelLogout: {enabled: true}} in nuxt.config.ts | Module Configuration |
| Spring Security | .oidcLogout(logout -> logout.backChannel(Customizer.withDefaults())) | Spring Security |
The Express and Node SDKs keep sessions on the server and delete the session the token names. The Next.js and Nuxt SDKs keep the session in a cookie, which the server cannot delete, so they record the logout and refuse every later request that presents a matching cookie. In every SDK the default record lives in the memory of one process; a deployment with more than one instance needs the shared store each SDK's reference describes.
Browser SDKs (@thunderid/browser, @thunderid/react, @thunderid/vue) and mobile SDKs cannot receive the request and rely on token revocation instead.
The rest of this page is for an application that receives the notification without one of these SDKs.
The Logout Token
The logout token is a signed JWT (JWS). ThunderID signs it with the same key and algorithm it uses for the client's ID tokens.
| Claim or header | Value |
|---|---|
typ header | logout+jwt |
iss | The issuer, the same value the client's ID tokens carry |
aud | The client id of the application this token is for |
sub | The subject identifier, the same value the client's ID tokens carry |
sid | The identifier of the session that ended. Always present. It matches the sid claim of the ID tokens issued in that session |
events | An object with one member, http://schemas.openid.net/event/backchannel-logout, whose value is an empty object |
jti | Unique per token, so a replayed token can be detected |
iat, exp | Issued at, and expiry. The default lifetime is 120 seconds |
nonce | Never present |
Because sid is always present, an application can end exactly the local session that joined the terminated ThunderID session. backchannel_logout_session_required is therefore not a registration parameter: every token carries sid.
Receive the Notification
The request ThunderID sends:
POST /backchannel-logout HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
logout_token=eyJhbGciOiJSUzI1NiIsInR5cCI6ImxvZ291dCtqd3QiLCJraWQiOiIuLi4ifQ...
Validate the Token
Reject the request with 400 unless every check passes:
- The body is
application/x-www-form-urlencodedand carries onelogout_token. - If the client is configured for ID token encryption, the token is a JWE. Decrypt it with the client's key, and run the remaining checks on the JWS inside.
- The token is a JWS whose
typheader islogout+jwt, signed with an algorithm the application accepts. - The signature verifies against a key from ThunderID's JWKS, selected by
kid. issequals the issuer the application was configured with, exactly.audcontains the application's client id.iatis not in the future andexphas not passed, within a small clock tolerance.eventscontainshttp://schemas.openid.net/event/backchannel-logoutwith an object value.nonceis absent. This check, with thetypandeventschecks, is what stops an ID token from being replayed as a logout token.jtiis present and has not been seen before.sidorsubis present.
Cache the JWKS, and bound how often an unknown kid triggers a refetch. The endpoint is unauthenticated, so an attacker who can reach it must not be able to make the application fetch keys on every request.
End the Local Session
- A token with a
sidends the local sessions that joined that ThunderID session. To make this possible, record thesidof the ID token against the local session at sign-in. - A token with only a
subends that subject's sessions that began before the token was issued. Allow for the clock tolerance here too: end a session that began within the tolerance afteriat, since ending one session too many is the safer mistake. A session the user started later stays. - Ending the session is local. Do not call the revocation or end session endpoint, which ThunderID has already handled, and do not redirect.
- A valid token that names no session the application holds is still a success. Answer
200, so a retry is harmless.
Respond
| Response | What ThunderID does |
|---|---|
200 or 204 | Records the delivery as done |
5xx, 429, a connection error or a timeout | Retries with a fresh token. 429 with a Retry-After header sets the wait, up to the longest wait of the backoff schedule, 4 seconds with the defaults |
Any other status, including 201, 202, any 3xx and any other 4xx | Records the delivery as failed and does not retry. Redirects are never followed |
Answer 5xx only when the logout could not be recorded, for example because a session store was unreachable. Answer 400 for a token that fails validation. Send Cache-Control: no-store on every response.
What Ends and What Does Not
- Back-channel logout ends the session the application manages. An access token the application already handed to a browser or to another service stays valid until it expires. ThunderID has revoked the tokens issued in the session, so a refresh fails, and a resource server that checks tokens with Token Introspection sees them as inactive.
- A browser-only application has no endpoint that can receive the request. It learns that the session ended when its next token refresh or API call fails.
- An application with several instances receives the notification on one of them. Keep whatever records the logout in a store the instances share.
Failure Behavior
| Setting | Default | Behavior |
|---|---|---|
| Timeout per attempt | 5 seconds | A slow endpoint is treated as unreachable for that attempt |
| Attempts | 3 | One delivery and two retries. Each attempt mints a fresh token with a new jti |
| Wait between attempts | 2 seconds, doubled | 2 seconds, then 4 seconds |
| Redirects | Never followed | A 3xx is a final failure |
| Response body | Discarded | Only the status code matters |
A failing endpoint affects nobody else: deliveries to other applications proceed, and the session termination itself never waits for a delivery. Every outcome is published as an observability event and, on failure, written as a warning log line with the client id, the session id and a reason code: unreachable, server_error, rejected, private_address, client_not_found, queue_full or shutdown.
Make the endpoint idempotent. A retry can arrive after the first delivery succeeded but its response was lost, and a token that names a session already ended must still get a 200.
Configuration
Every key is deployment level, in the server's deployment.yaml.
oauth:
logout:
backchannel:
enabled: true
token_validity_period: 120
request_timeout: 5
max_attempts: 3
retry_delay: 2
max_in_flight: 16
queue_size: 1024
reject_private_addresses: true
| Key | Default | Description |
|---|---|---|
enabled | true | Turns delivery on and off. Also drives both discovery flags. Registering a URI is the per-application opt-in, so enabling the feature starts no traffic by itself |
token_validity_period | 120 | Lifetime of a logout token, in seconds |
request_timeout | 5 | Seconds per delivery attempt. At most 30 |
max_attempts | 3 | Attempts per participant. 1 disables retry. At most 10 |
retry_delay | 2 | Seconds before the second attempt, doubled for each later one. At most 60 |
max_in_flight | 16 | Concurrent deliveries across all terminations |
queue_size | 1024 | Terminations waiting for delivery before new ones are dropped and recorded as queue_full |
reject_private_addresses | true | Rejects URIs that name localhost or a private, loopback, link-local or unspecified IP literal, and refuses to connect to such an address at delivery time. Set to false when relying parties run on a private network |
Try It in ThunderID
- Pick two server-side applications that sign in through the same authentication flow, and add the Check SSO Session and Save / Load Session nodes to that flow, see Single Sign-On for Flows. The default authentication flow has no session nodes, so it records no session and sends no notification.
- In the first application, add the receiving route. For an app built with the Express quickstart, add
app.post('/backchannel-logout', handleBackchannelLogout())afterthunderID(), and registerhttp://localhost:3000/backchannel-logoutas its back-channel logout URI. - A
localhostURI is rejected by default. For local testing only, setreject_private_addresses: falsein the server'sdeployment.yamland restart the server. - Sign in to the first application. Then sign in to the second application in the same browser, so both join one ThunderID session.
- Sign out of the second application. The first application's next request finds its session ended.
To watch a delivery, send a 400 from a test endpoint and look for the BACKCHANNEL_LOGOUT_FAILED event with reason rejected, or a 200 and the BACKCHANNEL_LOGOUT_DELIVERED event.
Related Guides
- RP-Initiated Logout, the sign-out request that most often triggers a notification
- Sessions and Single Sign-On, the session model and the
sidthat ties a logout token to a session - Server Metadata, where the capability is advertised
- JWKS, the keys that verify the logout token's signature
- Token Revocation and Token Introspection, how tokens issued in the ended session are invalidated