RP-initiated Logout und SSO Logout in OpenID Connect: So meldet Casdoor Benutzer ab
Die Abmeldung eines Benutzers von einer OpenID Connect-App erfordert mehr als das Löschen Ihres eigenen Sitzungs-Cookies. Der Benutzer ist beim Identitätsanbieter weiterhin angemeldet, sodass der nächste Klick auf "Sign in" direkt ohne Passwort durchgeht, und die anderen Apps, die diese Identitätsanbieter-Sitzung teilen, bleiben ebenfalls angemeldet. Eine ordnungsgemäße Abmeldung besteht aus drei Teilen: Löschen der lokalen Sitzung Ihrer App, Umleiten des Browsers zum end_session_endpoint des Anbieters (RP-initiated logout), damit die Anbieter-Sitzung beendet wird, und Ermöglichen, dass der Anbieter die anderen Apps über Back-Channel-Logout informiert. Bei Casdoor ist der End-Session-Endpunkt /api/logout, Back-Channel-Logout wird pro Anwendung konfiguriert, und Refresh-Tokens können unter /api/login/oauth/revoke widerrufen werden.
Abmeldemechanismen im Überblick
| Mechanismus | Wer löst ihn aus | Was er löscht | Casdoor-Unterstützung |
|---|---|---|---|
| Lokale App-Abmeldung | Ihre App | Nur das Sitzungs-Cookie Ihrer App und gespeicherte Tokens | Ihr Code |
| RP-initiated logout | Ihre App durch Umleiten des Browsers | Die Casdoor-Browser-Sitzung und das Token des id_token_hint | Ja, GET oder POST /api/logout, angekündigt als end_session_endpoint |
| Back-channel logout | Casdoor, Server-zu-Server | Die Sitzungen anderer Apps, jede App beendet ihre eigene | Ja, pro Anwendung Backchannel logout URL |
| Front-channel logout | Der Anbieter über versteckte iframes | Die Sitzungen anderer Apps über den Browser | Nein, Casdoor kündigt frontchannel_logout_supported nicht an |
| Token-Widerruf (RFC 7009) | Ihre App, Server oder Client | Ein Access- oder Refresh-Token sowie die Tokens, die dessen Refresh-Token teilen | Ja, POST /api/login/oauth/revoke, angekündigt als revocation_endpoint |
| Casdoor SSO Logout | Ihre App mit dem Token oder Cookie des Benutzers | Alle Sitzungen und Tokens des Benutzers oder nur die aktuelle Sitzung | Ja, /api/sso-logout, Casdoor-spezifisch |
| SAML single logout | Casdoor oder ein SAML Service Provider | Sitzungen in SAML-Apps | Ja, signierte LogoutRequest |
Lokale Sitzung vs. Identitätsanbieter-Sitzung
Bei jeder OIDC-Anmeldung sind mindestens zwei Sitzungen beteiligt, oft sogar drei:
- Die Sitzung Ihrer App. Üblicherweise ein Cookie, das den Benutzer und die von Ihnen vom Token-Endpunkt erhaltenen Tokens zuordnet.
- Die Casdoor-Sitzung. Ein Cookie auf der Casdoor-Domain. Solange es gültig ist, meldet Casdoor den Benutzer bei jeder App der Organisation ohne Passwort an, entweder nach einem Klick auf den Kontoauswahlbildschirm oder stillschweigend mit automatischer Anmeldung. Das ist es, was Single Sign-On ermöglicht.
- Eine Upstream-Identitätsanbieter-Sitzung, wenn sich der Benutzer bei Casdoor über Google, GitHub, einen Unternehmens-IdP usw. angemeldet hat.
Auth0 beschreibt in seiner Logout-Dokumentation dieselben drei Ebenen (Anwendungs-, Auth0- und Identitätsanbieter-Sitzungsebenen), und das Modell gilt für jeden OIDC-Anbieter.
"Lokalisierte Beendigung der Anwendungssitzung" beendet nur die erste Ebene: Ihre App vergisst den Benutzer, die Casdoor-Sitzung bleibt bestehen. Das passiert, wenn Sie einfach einen Cookie löschen. "Zentralisiertes Single Sign-Out" beendet die Casdoor-Sitzung und weist jede App des Benutzers an, ihre eigene Sitzung zu beenden. Die meisten Benutzer, die auf Sign out klicken, erwarten Letzteres, insbesondere auf einem geteilten Computer.
Für die dritte Ebene ruft Casdoor die Logout-URL eines Custom OAuth Providers auf, wenn die Anwendung Benutzer über diesen anmeldet und der Anbieter eine Logout-URL gesetzt hat. Es meldet den Benutzer nicht bei Google oder GitHub ab.
RP-initiated Logout mit dem End-Session-Endpoint
OpenID Connect RP-Initiated Logout 1.0 definiert, wie eine vertrauende Partei (Ihre App) den Anbieter auffordert, seine Sitzung zu beenden. Der Anbieter veröffentlicht den Endpoint als end_session_endpoint in seinem Discovery-Dokument, und die Anfrage enthält diese Parameter:
id_token_hint: Das ID-Token, das der Anbieter Ihrer App ausgestellt hat. Die Spezifikation markiert es als RECOMMENDED. Casdoor verwendet es, um genau dieses Token zu finden und ablaufen zu lassen.post_logout_redirect_uri: Wohin der Browser anschließend weitergeleitet wird. Die Spezifikation verlangt, dass er im Voraus registriert ist. Casdoor prüft ihn gegen die Redirect URLs der Anwendung und verweigert die Weiterleitung an andere Ziele.state: Ein undurchsichtiger Wert, den Casdoor an die Weiterleitung anhängt, damit Ihre App den eigenen Logout-Roundtrip von einer gefälschten Anfrage unterscheiden kann.client_id: Identifiziert die App, wenn kein ID-Token gesendet werden kann.
Die Spezifikation definiert auch logout_hint und ui_locales. Casdoor liest diese nicht ein.
Das Discovery-Dokument von Casdoor unter /.well-known/openid-configuration kündigt end_session_endpoint als https://<your-casdoor>/api/logout an, sodass OIDC-Bibliotheken, die RP-initiated Logout unterstützen, dies ohne Konfiguration übernehmen. Der Endpoint akzeptiert GET und POST, wie es die Spezifikation verlangt.
Was Casdoor kurz mit der Anfrage macht (die RP-initiated logout reference enthält alle Verzweigungen):
- Mit
id_token_hintsucht Casdoor das Token heraus, sendet Back-Channel-Logout-Tokens an die Anwendungen des Benutzers, lässt das Token ablaufen (sein Refresh-Token funktioniert dann ebenfalls nicht mehr), löscht die Casdoor-Sitzung im Browser und leitet aufpost_logout_redirect_urimitstateweiter. - Ohne
id_token_hintmeldet Casdoor die aktuelle Browsersitzung ab und lässt die darin ausgestellten Tokens ablaufen. Wenn der Browser keine Sitzung mehr hat, leitet er trotzdem weiter, wenn er die App kennt, entweder aus der Sitzung oder ausclient_id. - Ohne
post_logout_redirect_urigibt der Endpoint eine JSON-Antwort zurück, statt weiterzuleiten.
Beispiel: Sign-out-Schaltfläche in einer Browser-App
Eine Single-Page-App, die sich mit dem Authorization Code Flow und PKCE angemeldet hat, behält das ID-Token nach dem Token-Austausch. Der Sign-out-Handler löscht zuerst den lokalen Zustand und übergibt dann den Browser an Casdoor:
const ISSUER = "https://door.example.com";
const CLIENT_ID = "your-client-id";
async function signOut() {
const idToken = sessionStorage.getItem("id_token");
const discovery = await fetch(`${ISSUER}/.well-known/openid-configuration`)
.then((r) => r.json());
// 1. Forget the user locally, whatever happens next
sessionStorage.clear();
// 2. Remember a state value to check when Casdoor sends the browser back
const state = crypto.randomUUID();
sessionStorage.setItem("logout_state", state);
// 3. End the Casdoor session
const url = new URL(discovery.end_session_endpoint);
if (idToken) {
url.searchParams.set("id_token_hint", idToken);
} else {
url.searchParams.set("client_id", CLIENT_ID);
}
url.searchParams.set("post_logout_redirect_uri", "https://app.example.com/signed-out");
url.searchParams.set("state", state);
window.location.assign(url.toString());
}
// On https://app.example.com/signed-out
const returned = new URLSearchParams(window.location.search).get("state");
if (returned !== sessionStorage.getItem("logout_state")) {
console.warn("Unexpected logout redirect");
}
sessionStorage.removeItem("logout_state");
Fügen Sie https://app.example.com/signed-out zu den Redirect URLs der Anwendung in Casdoor hinzu, sonst gibt Casdoor eine Fehlermeldung zurück, statt weiterzuleiten. Eine serverseitig gerenderte App macht dasselbe von ihrer Logout-Route aus: Zerstören Sie die Server-Sitzung und antworten Sie dann mit einem 302 auf die End-Session-URL. Für Spring Security siehe den Spring Boot SSO post.
Back-Channel Logout: Die anderen Apps informieren
Das Beenden der Casdoor-Sitzung verhindert neue stille Anmeldungen, aber App B hat immer noch ihren eigenen Sitzungs-Cookie von vor einer Stunde. Etwas muss App B informieren. OpenID Connect Back-Channel Logout 1.0 erledigt dies Server-zu-Server: Der Anbieter sendet ein POST mit dem Formularparameter logout_token, einem signierten JWT, der den Benutzer benennt, an die registrierte Back-Channel-URI jeder App.
In Casdoor setzen Sie die Backchannel logout URL auf der Bearbeitungsseite der Anwendung. Wenn sich ein Benutzer über /api/logout abmeldet, sendet Casdoor ein Logout-Token an jede Anwendung, die ein aktives Token für diesen Benutzer hält und diese URL gesetzt hat. Das Token ist mit dem Zertifikat der Anwendung signiert, demselben Schlüssel wie für seine ID-Tokens, und enthält:
issundaud: Dieselben Issuer- und Client-ID-Werte wie in den ID-Tokens der Appsub: Die Benutzer-ID, derselbe Wert wiesubim ID-Tokeniatundjtieventsmit dem Mitgliedhttp://schemas.openid.net/event/backchannel-logoutsidnur für bestimmte Logouts, wie SSO logout, und nicht für/api/logout
Casdoor's ID tokens don't carry a sid claim, so key your local sessions by sub and end all of them for that user. The logout token expires two minutes after iat, so a validator that requires exp, as the spec does, accepts it. Casdoor sends each token once, with a 30-second timeout, and doesn't retry. For applications outside the built-in organization, it refuses to call loopback and link-local addresses, so http://localhost won't receive logout tokens during development.
Beispiel: Ein Back-Channel-Logout-Empfänger
Dieser Express-Handler folgt den Validierungsschritten der Spezifikation unter Verwendung der jose-Bibliothek:
import express from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
// Take both values from the discovery document of your Casdoor
const ISSUER = "https://door.example.com";
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks`));
const CLIENT_ID = process.env.CASDOOR_CLIENT_ID;
const EVENT = "http://schemas.openid.net/event/backchannel-logout";
const seenJti = new Map();
const app = express();
app.post(
"/oidc/backchannel-logout",
express.urlencoded({ extended: false }),
async (req, res) => {
res.set("Cache-Control", "no-store");
try {
const { payload } = await jwtVerify(req.body.logout_token, JWKS, {
issuer: ISSUER,
audience: CLIENT_ID,
maxTokenAge: "2m",
});
if (!payload.events || typeof payload.events[EVENT] !== "object") {
throw new Error("not a logout token");
}
if (!payload.sub && !payload.sid) {
throw new Error("no sub or sid");
}
if ("nonce" in payload) {
throw new Error("nonce is not allowed");
}
if (seenJti.has(payload.jti)) {
throw new Error("replayed token");
}
seenJti.set(payload.jti, Date.now());
// Your own helper: delete every session row that belongs to this user
await destroySessionsForUser(payload.sub);
res.sendStatus(200);
} catch (err) {
res.status(400).json({ error: "invalid_request", error_description: err.message });
}
}
);
jwtVerify prüft die Signatur, iss, aud und mit maxTokenAge den iat-Claim. Um Sitzungen nach Benutzer zu finden, speichern Sie sub aus dem ID-Token bei der Anmeldung in jedem Sitzungsdatensatz. Bereinigen Sie seenJti regelmäßig oder halten Sie es in Redis.
Widerruf von Refresh-Tokens
Ein Refresh-Token überlebt die Sitzung, die es erstellt hat. Wenn Ihre App eines behält, hinterlässt ein Logout, der es nie berührt, einen Anmeldeausweis, der tagelang neue Access-Tokens ausstellen kann.
Der End-Session-Aufruf mit id_token_hint lässt dieses Token bereits ablaufen, einschließlich des Refresh-Tokens. Widerrufen Sie es explizit, wenn der Logout nicht über den Browser erfolgt, z. B. in einer CLI, einer Desktop- oder Mobil-App, oder wenn der Benutzer nur Ihre App verlässt und die Casdoor-Sitzung behält. Casdoor implementiert RFC 7009 am revocation_endpoint seines Discovery-Dokuments:
curl -X POST https://door.example.com/api/login/oauth/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$REFRESH_TOKEN" \
-d token_type_hint=refresh_token
Ein öffentlicher Client, der PKCE verwendet, kann client_id im Body ohne Secret senden. Das Widerrufen eines Refresh-Tokens führt auch zum Ablauf der damit ausgegebenen Access-Tokens. Danach antwortet der Refresh-Endpoint mit invalid_grant, und die Introspection liefert "active": false; When Casdoor revokes tokens listet alle Ereignisse auf, die dies bewirken.
Eine Einschränkung gilt für jeden Provider, der JWT-Access-Tokens ausstellt: Eine API, die nur die Signatur und exp prüft, akzeptiert ein widerrufenes Access-Token weiterhin bis zu dessen Ablauf. Halten Sie Access-Tokens kurzlebig oder lassen Sie die API Token-Introspection aufrufen, wenn sie eine Abmeldung sofort bemerken muss.
Single Sign-out über mehrere Apps hinweg
Mit den oben beschriebenen Komponenten funktioniert Single Sign-out wie folgt, wenn der Benutzer in App A auf "Sign out" klickt:
- App A löscht seine Sitzung und leitet auf
/api/logoutweiter, zusammen mitid_token_hint. - Casdoor sendet Logout-Tokens an die Back-Channel-URLs der anderen Apps des Benutzers, lässt das Token ablaufen und beendet die Casdoor-Browsersitzung.
- Apps B und C empfangen das Logout-Token und löschen ihre Sitzungen für diese
sub. - Der Browser landet auf der Abmeldeseite von App A. Wenn App B nun geöffnet wird, zeigt sich die Casdoor-Anmeldeseite.
Eine App ohne Back-Channel-URL bleibt angemeldet, bis ihre eigene Sitzung oder ihr Access-Token abläuft. Wenn Sie keinen Empfänger zur App hinzufügen können, halten Sie deren Sitzungen kurz oder sorgen Sie dafür, dass sie das Token bei jeder Anfrage validiert.
Verwenden Sie für "melden Sie mich überall ab" nach einem verlorenen Laptop den SSO-Logout-Endpoint von Casdoor mit logoutAll=true: Dies lässt jedes Token ablaufen und löscht jede Sitzung des Benutzers und sendet außerdem signierte Benachrichtigungen über Notification-Provider an Apps, die kein OIDC sprechen. Administratoren können einzelne Sitzungen auf der Seite Sessions beenden (Session Management). Für SAML-Apps siehe SAML single logout. Hintergrundwissen: single logout und SAML single logout auf www.casdoor.com.