Implementing Back-Channel Logout

When a user signs out of one application, the others in the same single sign-on estate usually carry on as if nothing happened. Back-channel logout fixes that with a server-to-server notification, and unlike its front-channel predecessor it does not depend on third-party cookies or on the browser staying open. This page is part of the OAuth 2.0 token revocation guide.

Front Channel Versus Back Channel

Both mechanisms answer the same question — how does application B learn that the user signed out of application A — and they differ in reliability.

Front-channel and back-channel logout compared Front-channel logout loads each application in a hidden frame and depends on third-party cookies and the browser remaining open; back-channel logout posts a signed token server to server and works regardless of the browser. Front channel hidden iframes per application needs third-party cookies silently fails if the tab closes Back channel signed POST, server to server independent of the browser retryable and verifiable
Front-channel logout was designed for a web where third-party cookies worked. Back-channel logout is the mechanism that still functions.

Registering and Receiving

Register a logout endpoint with your provider and mark the client as supporting back-channel logout, including whether you want a session identifier in the token. The provider then posts a JSON Web Token — the logout token — to that endpoint whenever a session it belongs to ends.

The endpoint is unauthenticated in the usual sense: anyone can post to it, so the token is the only thing establishing trust. Validate it with the same rigour as an ID token, plus the checks specific to logout.

export async function backChannelLogout(req: Request, res: Response) {
  const token = req.body.logout_token;
  if (!token) return res.status(400).end();

  const { payload } = await jwtVerify(token, JWKS, {
    issuer: process.env.ISSUER!,
    audience: process.env.CLIENT_ID!,
    algorithms: ["RS256", "ES256"],
  });

  // Logout-specific requirements, not present on an ID token.
  if (payload.events?.["http://schemas.openid.net/event/backchannel-logout"] === undefined) {
    return res.status(400).end();                 // wrong event type
  }
  if (payload.nonce) return res.status(400).end();  // a logout token must NOT carry a nonce
  if (!payload.sid && !payload.sub) return res.status(400).end();

  await endSessionsFor({ sid: payload.sid as string, sub: payload.sub as string });
  res.status(200).end();                            // acknowledge only after the work is done
}

The nonce rule catches a genuine attack: an ID token replayed as a logout token would otherwise let anyone who obtains one terminate a user’s sessions. The event claim is the other half of that check, and both are cheap.

Mapping the Notification to Your Sessions

The token names the provider’s session (sid) or the user (sub), and you have to translate that into your own session records — which only works if you stored the mapping when the session was created.

Mapping a provider session identifier to local sessions At login the provider session identifier is stored on the local session record and indexed, so a logout token naming that identifier resolves directly to the sessions to end. At login store sid on the session Index by sid one provider session On logout token end exactly those sessions Without the mapping, a sid-only token leaves you guessing — or ending every session for the user.
Capture the provider session identifier at login even if you do not yet implement logout; retrofitting it means existing sessions cannot be matched.

If the token carries only sub, you must end every session for that user, which is heavier-handed than intended but correct. Where both are present, prefer sid: a user with sessions from two separate provider logins should not lose both because one ended.

Idempotency, Retries and Ordering

Providers retry on failure, so the same logout token can arrive several times, and network conditions mean it can arrive out of order relative to other events.

Make the handler idempotent: ending a session that is already ended is a success, not an error. Record the token’s jti briefly and treat a repeat as an acknowledged no-op, which also prevents a replayed token from doing anything beyond the first delivery.

Return 200 only after the work is complete, and return a 5xx if it is not, so the provider retries rather than assuming success. That means the handler must be fast — do the session deletion synchronously and defer anything slow, such as notifying other systems, to a queue.

Guard against the ordering problem: a logout token can theoretically arrive before your callback has finished creating the session it refers to. Handle that by recording the sid as terminated for a short window, so a session created moments later for the same provider session is rejected rather than left alive.

Monitoring It

Back-channel logout fails silently by nature, because nobody is watching the endpoint when it stops working. Track three things: the rate of logout tokens received, the rate of validation failures broken down by reason, and the number of local sessions ended per token.

A drop to zero in the first usually means the provider stopped sending — a configuration change, an expired registration, a URL that moved. Validation failures grouped by reason separate a signing-key rotation from an attacker posting rubbish at your endpoint. And a token that ends zero sessions every time means your mapping is broken, which is the failure mode that looks healthiest from the outside.

Rolling It Out Across an Estate

Order of work when adopting back-channel logout Capture the provider session identifier everywhere first, then implement and register the endpoint one application at a time, verifying each with a real logout before continuing. 1 · capture sid everywhere, first 2 · one endpoint verify with a real logout 3 · expand application by application Skipping step one means the first notifications arrive and match nothing.
Step one is invisible to users and safe to ship on its own, which makes it the natural first move.

Adding back-channel logout to several applications at once is mostly coordination, and doing it in the wrong order produces confusing partial behaviour that is hard to attribute.

Start by capturing the provider session identifier at login everywhere, before any application implements the endpoint. That change is invisible to users, safe to deploy independently, and it is the prerequisite the rest depends on — retrofitting it later means existing sessions cannot be matched and the first notifications appear to do nothing.

Then implement and register the endpoint one application at a time, verifying each with a real logout before moving on. Because the provider notifies every registered client, a broken endpoint in one application does not affect the others, but it does produce retries and error volume that will confuse whoever is watching the provider’s logs.

Announce the behaviour change internally. Signing out of one application now signs the user out of the others, which is the intent, and it will still surprise people who have grown used to the previous behaviour — particularly support and operations staff who keep several tools open.

Finally, decide what “signed out” means for applications with their own long-lived state. A background job running on a user’s behalf, a scheduled export, a webhook subscription: none of these are sessions, and back-channel logout does not touch them. Write down which of them should stop when a user signs out and which should continue, because the answer differs by product and the question will otherwise be answered by accident.

Add the endpoint to whatever synthetic monitoring you already run, posting a deliberately invalid token and asserting a 400 rather than a timeout or a 500. That confirms the route exists and the handler is reachable without needing a real logout, and it distinguishes “nobody signed out today” from “the endpoint has been returning 404 since the last deploy”.

Finally, keep the endpoint’s URL stable and documented alongside the rest of your provider configuration. It is registered once, called by a system nobody logs into, and therefore easy to break with a routing change that nothing else notices — which is exactly why the received-token metric belongs on a dashboard rather than in a log.

Frequently Asked Questions

Does the logout endpoint need authentication?

The token is the authentication. The endpoint is publicly reachable — the provider calls it server to server, without a session — so its safety rests entirely on validating the signature, issuer, audience and event claim. Rate-limit it, log rejections, and never take any action on a token that fails validation, including logging its contents verbatim.

What if the logout token has no sid?

Then it identifies the user rather than a specific provider session, and the correct response is to end every local session for that user. It is broader than ideal, which is why capturing sid at login is worth doing — but ending too much is the safe direction to err in when a user has asked to sign out.

Should back-channel logout revoke refresh tokens too?

Yes. Ending the local session while leaving a refresh token alive means a background process can mint fresh access tokens for a user who has signed out. Treat the notification as a full termination: delete the session records, revoke the associated refresh-token families, and bump any version counter used to invalidate cached credentials.

How do I test it?

Sign in through the provider, capture the sid, then trigger a logout at the provider and assert that your endpoint received a token and that the corresponding session no longer authenticates. Add unit tests for the rejection paths — wrong audience, missing event claim, a nonce present, an unknown signing key — because those are the checks that protect an endpoint anyone can reach.

What happens if my endpoint is down when the user logs out?

The provider retries according to its own policy, and if the retries are exhausted the session stays alive locally until it expires. That residual risk is the argument for keeping session lifetimes short even with back-channel logout in place, and for alerting on a drop in received tokens so an endpoint outage is noticed rather than discovered later.