BrandApp Playground

Single Logout

OIDC Single Logout ties session termination in both directions. Receive Back-Channel Logout with createBackchannelLogoutHandler (reopt → your app), and trigger RP-Initiated Logout with buildEndSessionUrl (your app → reopt). Verification is zero-dependency (Web Crypto Ed25519); the dev server signs real tokens so the demo runs offline.

New in 3.2.0 — /logout sub-path.

The simulator seeds a local session tagged with a reopt sid, mints a real EdDSA logout_token from the in-memory dev IdP, and POSTs it form-encoded to the actual /api/auth/backchannel-logout receiver — the same wire shape reopt uses in production. Valid tokens revoke the session; each reject scenario exercises a different verify branch.

1 · Back-Channel Logout (reopt → your app)

Receive server-to-server notification and end the matching local sessions with createBackchannelLogoutHandler.

Local sessions (your app)

No local sessions. Seed one to represent a signed-in user whose session row carries a reopt sid.

Scenario (targets the newest session)

Seed a session first — the back-channel logout revokes sessions by their reopt sid.

2 · RP-Initiated Logout (your app → reopt)

End the reopt session too by sending the user through buildEndSessionUrl.

Must be pre-registered on your OAuth client for reopt to redirect back

Echoed back on the return URL — carry a CSRF token here

app/api/auth/backchannel-logout/route.ts
import { createBackchannelLogoutHandler } from "@reopt-ai/brandapp-sdk/logout";

// Web-standard (Request) => Response — mount in Next.js, Hono, or any fetch server.
// The handler verifies the EdDSA logout_token against reopt's discovery + JWKS
// (Web Crypto, zero deps), then calls onLogout on success.
export const POST = createBackchannelLogoutHandler({
  onLogout: async ({ sid, sub }) => {
    // End every LOCAL session derived from this reopt session id.
    // Make it idempotent — reopt retries on any non-2xx delivery.
    await db.sessions.deleteMany({ where: { reoptSid: sid } });
  },
});

// Response contract:
//   200 valid + onLogout ok        501 onLogout threw (reopt retries)
//   400 invalid/expired token      503 discovery/JWKS briefly unreachable
//   405 non-POST                        (retryable — NOT a bad token, 3.3.0)
// All responses carry Cache-Control: no-store (spec requirement).