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
| Event | What happens | What you do |
|---|---|---|
| User ends the session from their phone, or an admin revokes it | We POST a signed logout token to your backchannelLogoutURI | Destroy the local session for that sid |
| User signs out of your app | Nothing reaches us automatically | POST to our end-session endpoint, or leave our session running — your choice |
| Our session expires | Nothing is sent | Nothing. 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
- Your app registers a
backchannelLogoutURI(e.g.https://yourapp.com/auth/backchannel-logout). - When a session ends on 0account's side, 0account POSTs a signed Logout Token (JWT) to that URI.
- 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:
| Claim | Value |
|---|---|
iss | https://v1.0account.com |
aud | Your client_id |
sub | User's profile ID |
sid | Session ID |
iat | Issued-at (Unix timestamp) |
jti | Unique 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
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>.
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.