Back-channel logout
Back-channel logout lets FerrisKey tell an application’s server that a user’s session has ended, so the application can drop its own session without waiting for the browser to come back.
Without it, an application only learns about a logout when the user clicks its own sign-out button. A session revoked by an administrator, a password reset, or a disabled account would leave the application signed in until its local session times out. With it, FerrisKey sends a signed logout token straight to the application, server to server, whatever ended the session.
Registering an endpoint
Open the client, then Settings, then the Logout section.
| Console label | API field | Meaning |
|---|---|---|
| Back-channel logout URL | backchannel_logout_uri | Where FerrisKey POSTs the logout token. Leave it empty to opt out |
| Require session id (sid) | backchannel_logout_session_required | Records that the application needs the sid claim. Defaults to true |
Through the API, send both fields to PATCH /realms/{realm_name}/clients/{client_id}. Leaving backchannel_logout_uri out keeps the current value, and sending null clears it.
{
"backchannel_logout_uri": "https://app.example.com/backchannel-logout"
}
The URL is checked for shape when you save it: it needs a host, no embedded credentials, and the https scheme (http is accepted only for a loopback host). A bad value is refused with 422 on the field backchannel_logout_uri. Whether the address may actually be reached is decided at send time, against the address the host resolves to then.
The session id is always part of the token. backchannel_logout_session_required is stored for the record and does not change what FerrisKey sends.
Discovery advertises the feature: /realms/{realm_name}/.well-known/openid-configuration carries backchannel_logout_supported and backchannel_logout_session_supported, both true.
When a logout token is sent
FerrisKey sends one whenever a user session is deleted, except when it simply expires. That covers:
- the logout endpoint,
GETorPOST /realms/{realm_name}/protocol/openid-connect/logout; - a session revoked with
DELETE /realms/{realm_name}/users/{user_id}/sessions/{session_id}; - a user disabled or deleted, including bulk deletion;
- a password reset, by an administrator or through the reset link, a password update completed through the required action, and a password change from the account page, which signs out the user’s other sessions.
The recipients are the clients that took part in the session: every enabled client with a backchannel_logout_uri that was issued an access token carrying that session’s sid. A client that never received a token in the session is not contacted, and neither is one that has no endpoint. A client_credentials token has no session, so it triggers nothing.
If a client received tokens under two different issuer URLs in the same session, it gets one logout token per issuer.
What FerrisKey sends
A POST with Content-Type: application/x-www-form-urlencoded and a single field, logout_token:
POST /backchannel-logout HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
logout_token=eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...
The token is a JWT signed with the realm key that signs every other token, so the application verifies it against the same JWKS (the jwks_uri of the discovery document). The header carries the key id and a distinctive type:
{ "alg": "RS256", "kid": "5c1e0b7a-...", "typ": "logout+jwt" }
{
"iss": "https://sso.example.com/realms/home",
"sub": "8d1f3c52-0a4e-4d1c-9a57-2f0a1c9e7b11",
"aud": "orders-web",
"iat": 1790000000,
"exp": 1790000120,
"jti": "0192f7c4-5e1a-7b3e-9d0c-3a6f1b2c4d5e",
"sid": "b2a4c6e8-1f3d-4a5b-8c7e-9d0f1a2b3c4d",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}
| Claim | Value |
|---|---|
iss | The issuer of the tokens the client received in that session, so it matches the iss the application already sees |
sub | The user’s id |
aud | The client’s client_id, as a single string |
iat, exp | Issue time, and 120 seconds later |
jti | A fresh identifier per token |
sid | The session id. The same value is the sid claim of the access and ID tokens that session produced |
events | The back-channel logout event member |
There is never a nonce.
What the application must check
OpenID Connect Back-Channel Logout 1.0 asks the receiver to validate the token before acting on it. In practice:
- Verify the signature with a key from the JWKS, picked by
kid, and check that thetypheader islogout+jwt. - Check that
issis the realm’s issuer, thataudcontains yourclient_id, and thatiatandexpput the token in the present. - Check that
eventscontains thehttp://schemas.openid.net/event/backchannel-logoutmember, thatsuborsidis present, and that there is nononce. - Reject a
jtiyou have already seen.
Then end every local session tied to that sid and answer with a 2xx. FerrisKey treats any 2xx as delivered. Answer 400 when the token is invalid: that is the status the specification asks for, and FerrisKey does not retry it.
Delivery
The send is asynchronous. Logging out never waits for an application, and never fails because one is slow or down.
| Behaviour | Value |
|---|---|
| Attempts | 3, waiting 0, 2 and 10 seconds before each |
| Retried | Connection, DNS and timeout failures, and 5xx answers |
| Not retried | Any other status, 4xx included, and a refused address or unusable URL |
| Redirects | Never followed. A 3xx counts as a refusal |
| Timeouts | 5 seconds to connect, 10 seconds per request |
| Queue | 1024 ended sessions held in memory, delivered 16 at a time |
Delivery is best effort. A client that stays unreachable through the three attempts misses the notice, and so does one whose notice was queued when the server restarted. When the queue is full, new sessions are dropped with a warning in the log and the user’s logout still succeeds. Keep the application’s own session lifetime bounded as the backstop.
FerrisKey resolves the host once and pins the connection to an address it may reach, with the same guard that protects webhook deliveries. An https endpoint is required. Loopback, private and other non-public addresses are refused, and link-local addresses, which include the cloud metadata address, are refused whatever the settings. WEBHOOK_ALLOW_PRIVATE_ENDPOINTS=true lets loopback and private addresses through for local development and allows plain http to loopback only. It is the same switch as for webhooks, and it should stay off in production. See Configuration.
Failures are written to the server log, not to SeaWatch. The messages to look for:
| Log message | Meaning |
|---|---|
Logout token not delivered, retrying | A retryable failure, with the attempt number |
Logout token refused | The endpoint answered with a status that is not retried, or the address was refused |
Gave up delivering a logout token | The three attempts failed |
Back-channel logout queue full, dropping a session | The queue was full |
Could not sign a logout token | The realm key could not sign the token |