Skip to main content

Logout (Back-Channel & RP-Initiated)

A session exists in two places once someone signs in: yours, and ours. Ending one does not end the other by itself. This page is the contract for keeping them in step.

Who tells whom

EventWhat happensWhat you do
User ends the session from their phone, or an admin revokes itWe POST a signed logout token to your backchannelLogoutURIDestroy the local session for that sid
User signs out of your appNothing reaches us automaticallyPOST to our end-session endpoint, or leave our session running — your choice
Our session expiresNothing is sentNothing. By then every token tied to it has expired anyway

Two consequences worth planning for.

We do not see your logout unless you tell us. Signing a user out of your own app ends your session and nothing else. To end theirs here as well, call the end-session endpoint with the ID token you were issued:

POST https://v1.0account.com/oauth/logout
id_token_hint=<the id_token>

No browser and no redirect — your server can call it directly. OIDC defines RP-initiated logout as a front-channel redirect, which we also accept; the POST form exists because ending a session should not require the user to be present. See session termination below.

sid is the join key, and it is stable. It arrives in the ID token, it is what a logout token names, and it does not change while the session lives. Store it next to your own session record — without it you cannot act on a logout token, because sub alone would log the user out of every device at once.

Expiry is silent on purpose. We do not notify when a session simply ages out. Your session has its own lifetime, which you chose; by the time ours lapses every access and refresh token tied to it has already stopped working, so a notification would tell you nothing you could not already see. If you want to detect revocation between logout tokens, poll /oauth/userinfo — see notifying the active browser.

How it works

  1. Your app registers a backchannelLogoutURI (e.g. https://yourapp.com/auth/backchannel-logout).
  2. When a session ends on 0account's side, 0account POSTs a signed Logout Token (JWT) to that URI.
  3. Your endpoint verifies the token and destroys the matching local session.

Logout Token

The Logout Token is a short-lived JWT signed the same way as ID tokens. Verify it the same way. Required claims:

ClaimValue
isshttps://v1.0account.com
audYour client_id
subUser's profile ID
sidSession ID
iatIssued-at (Unix timestamp)
jtiUnique token ID
events{"http://schemas.openid.net/event/backchannel-logout": {}}

Go — Receiver Endpoint

package main

import (
"context"
"net/http"

"github.com/coreos/go-oidc/v3/oidc"
)

var logoutVerifier *oidc.IDTokenVerifier

func init() {
provider, err := oidc.NewProvider(context.Background(), "https://v1.0account.com")
handleError(err)
logoutVerifier = provider.Verifier(&oidc.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
// add more config if needed (e.g. RedirectURIs, etc.)
})
}

// POST /auth/backchannel-logout
func handleBackchannelLogout(w http.ResponseWriter, r *http.Request) {
logoutToken := r.FormValue("logout_token")
if logoutToken == "" {
http.Error(w, "missing logout_token", http.StatusBadRequest)
return
}

token, err := logoutVerifier.Verify(r.Context(), logoutToken)
if err != nil {
http.Error(w, "invalid logout token", http.StatusUnauthorized)
return
}

var claims struct {
UserID string `json:"sub"`
SID string `json:"sid"`
Events map[string]any `json:"events"`
}
token.Claims(&claims)

if _, ok := claims.Events["http://schemas.openid.net/event/backchannel-logout"]; !ok {
http.Error(w, "not a logout token", http.StatusBadRequest)
return
}

// Destroy session by UserID (sub) or Session ID (sid)
destroySessionByUserID(claims.UserID)
w.WriteHeader(http.StatusOK)
}

Node.js — Receiver Endpoint (openid-client)

const { Issuer } = require("openid-client");
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));

let client;
async function init() {
const issuer = await Issuer.discover("https://v1.0account.com");
client = new issuer.Client({
client_id: process.env.ZERO_CLIENT_ID,
client_secret: process.env.ZERO_CLIENT_SECRET,
});
}

// POST /auth/backchannel-logout
app.post("/auth/backchannel-logout", async (req, res) => {
try {
const token = await client.validateBackchannelLogoutToken(
req.body.logout_token,
);
// token.sid — session ID; token.sub — user ID
await destroySession(token.sid);
res.sendStatus(200);
} catch (err) {
res.sendStatus(400);
}
});

init().then(() => app.listen(3000));

Session Termination (Server-to-Server)

Rather than redirecting the user's browser to 0account, your backend can call the logout endpoint directly. POST with only id_token_hint and 0account returns 200 OK — no redirect involved.

Go:

http.PostForm("https://v1.0account.com/oauth/logout", url.Values{
"id_token_hint": {storedIDToken},
})

Node.js:

await fetch("https://v1.0account.com/oauth/logout", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ id_token_hint: storedIdToken }),
});

This is the recommended approach. The flow becomes:

Frontend  →  DELETE /auth/logout  (your backend)
Backend → POST https://v1.0account.com/oauth/logout (server-to-server)
← 200 OK
Backend → destroy local session, respond to frontend
Frontend ← redirect wherever you want
RP-Initiated Logout (browser redirect)

If you need the user's browser to land on a specific post-logout page served by 0account, you can redirect to the end_session_endpoint instead:

https://v1.0account.com/oauth/logout
?id_token_hint=<id-token>
&post_logout_redirect_uri=https://yourapp.com/logged-out
&state=<optional-random>

post_logout_redirect_uri must match a URI registered for your app. After terminating the session, 0account redirects the browser to post_logout_redirect_uri?state=<state>.

tip

Register backchannelLogoutURI to handle logout events triggered from outside your app (mobile app, another device, 0account dashboard), and call the server-to-server logout endpoint when the user explicitly signs out in your UI.