SenseCrypt Docs
Guides

Back-channel logout

Receive OIDC Back-Channel Logout tokens — registering the URL, choosing which session endings you are told about, the logout token's claims, delivery and retries, and the checks your endpoint must make.

Clearing your own session cookie when the user signs out of your application is RP-initiated logout. Back-channel logout is the other direction: SenseCrypt tells you, server-to-server, that a session your application was signed in to has ended — because another application in the same browser signed out, or because something happened to the user.

Register a back-channel logout URL on the application and SenseCrypt POSTs a signed logout_token to it. Discovery advertises backchannel_logout_supported and backchannel_logout_session_supported, both true.

Register the URL

Set backchannel_logout_uri on the OIDC application. It must be an absolute https URL, and its host must not be a private, loopback, link-local or otherwise reserved address — a http URL is refused 422 with code: "url.https_required" and an internal host with code: "url.private_host".

The host is screened again at delivery: the name is re-resolved and the connection is pinned to a validated address, so a name that only resolves inward later is refused then too. Redirects are never followed.

Optionally set backchannel_logout_session_required: true if your implementation needs every token to name a session. SenseCrypt always sends one, so the flag is always satisfied (see The logout token).

Choose the endings you hear about

backchannel_logout_initiators is the list of session endings this application wants a token for. It defaults to ["rp-logout"], so registering a URL does not start a flood of tokens for endings you never asked about.

InitiatorThe ending it namesOpt-in?
rp-logoutA relying party asked — /logout with an id_token_hint, or the confirmed cookie-only logout.Always on. Registering the URL is the opt-in, so the list must contain it.
session-revokedThe session ended for a reason other than a lifecycle event: a SCIM active: false suspend, or a different user authenticating in the same browser (an account switch).Yes
access-revokedThe user lost access to this application through an authorization change: removed from a group that granted it (console or SCIM), the group deleted, or the group detached from the application. Scoped to the application that lost the user — a member who still reaches other applications keeps the browser session and only the applications they lost are told; a member left in no app-attached group has the session ended and every opted-in application told.Yes
account-deletedThe user's account is gone — a user delete from the console or over SCIM.Yes
email-identifier-changedThe user's email identifier was changed — from the console or over SCIM — which severs the credentials and sessions bound to the old address.Yes
session-expiredThe session reached its absolute expiry and was reaped.Yes

Send the list on create or PATCH; it is de-duplicated and stored in the order above. A value outside the vocabulary is refused 422 with code: "backchannel_logout_initiators.invalid", and a list that drops rp-logout with code: "backchannel_logout_initiators.requires_rp_logout" — it is refused rather than silently repaired.

idp-logout and password-changed (names you may know from other providers) are not offered: there is no upstream federation on this plane, and no password to change.

What is not announced

  • Tenant deletion. It retires the tenant's signing keys and revokes its applications in the same transaction, so no relying party could verify a token afterwards.
  • An account holder deleting their own account. That is a reactivatable request that retires nothing.
  • Deleting the application. The delete revokes the application in the same transaction, and an announcement only ever goes to a live application — so the logins it had are dropped silently. Note what does not stop the token: the registered backchannel_logout_uri survives the delete untouched, so do not assume a delete unregisters your endpoint. Detaching a group from the application is announced; that is access-revoked above.

The logout token

// header
{ "alg": "ES256", "kid": "<tenant key id>", "typ": "logout+jwt" }
// payload
{
  "iss": "https://acme.example.com",
  "sub": "b1e6…c9",
  "aud": "your-client-id",
  "iat": 1893455400,
  "exp": 1893455520,
  "jti": "9f2c…",
  "events": { "http://schemas.openid.net/event/backchannel-logout": {} },
  "sid": "d4c1…"
}
  • typ is logout+jwt and there is no nonce. Those two checks are what stop a user's own ID token being posted to your endpoint to sign someone out.
  • Every token carries sid — the same sid your application saw in that session's ID token. SenseCrypt never sends a sub-only token: the scope of a token is exactly the scope of what it revoked, which is one session. Do not write a branch that treats a missing sid as "every session for this user".
  • aud is your client_id; iss is the issuer host the session was established on.
  • exp is about two minutes after iat. The token is minted per delivery attempt, so a retry brings a fresh iat, exp and jti — a jti replay cache on your side will not reject a legitimate retry.
  • Signed with the tenant key for your application, using the algorithm you registered as id_token_signed_response_alg, and read live at delivery — so rotating a key or changing the algorithm takes effect on the next attempt.

RP-initiated logout fans tokens out to every application in that browser session that registered a URL, including the one that asked. Your endpoint will receive a token for a session you have already ended yourself; treat it as a no-op rather than an error.

Delivery

SenseCrypt POSTs to your URL directly, as application/x-www-form-urlencoded:

POST /backchannel-logout HTTP/1.1
Host: yourapp.com
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-store

logout_token=eyJhbGciOiJFUzI1NiIsInR5cCI6ImxvZ291dCtqd3QifQ...
  • 200 or 204 means delivered. Any other status — and any transport failure — is a failure, retried with backoff up to 5 attempts in total, after which the notification is abandoned. Unlike the CIBA callback contract, a 4xx is not final here: a 400 is the only way to tell us our token was wrong, so it is retried like any other failure. Answer 200 for anything you have handled, including a session you no longer know about.
  • Delivery is queued, not inline. The user's logout redirect never waits on your endpoint, and a slow or dead relying party does not hold up anyone else's token. Expect the POST shortly after the ending, not synchronously with it.
  • Redirects are never followed — a 3xx is just a failure.

What your endpoint must check

Validate before you act (OpenID Connect Back-Channel Logout 1.0 §2.6):

  1. Signature — against the tenant's jwks_uri, selecting the key by the header's kid. Tolerate more than one published key: a rotation grace window publishes the outgoing key alongside the new one.
  2. iss equals the tenant issuer you integrated against, and aud equals your client_id.
  3. iat is recent and exp is in the future. The lifetime is short, so do not queue a token for later processing.
  4. events contains the member http://schemas.openid.net/event/backchannel-logout.
  5. typ is logout+jwt and there is no nonce claim. Refuse the token if either is wrong — this is the ID-token-substitution check.
  6. sid is present. That is the session to end.
  7. Optionally, refuse a repeated jti as a replay. Keep the window short: a retried delivery is a new jti, but a genuine replay is not.

Then end the session on your side — delete the session row your store keeps for that sid — and answer:

HTTP/1.1 200 OK
Cache-Control: no-store

On a token you refuse, answer 400 with a JSON error body. Never answer with a redirect, and never require a browser cookie: this request comes from SenseCrypt's servers, not from the user's browser, so it carries none of the user's cookies and must not be routed through any browser-session middleware or CSRF check.

On this page