Passer au contenu principal

Déconnexion initiée par le RP et déconnexion SSO dans OpenID Connect : comment Casdoor déconnecte les utilisateurs

· Lecture de 12 minutes
Yang Luo
Casdoor Maintainer

Déconnecter un utilisateur d'une application OpenID Connect nécessite plus que la suppression de votre propre cookie de session. L'utilisateur est toujours connecté au fournisseur d'identité, donc le prochain clic sur "Sign in" passe directement sans mot de passe, et les autres applications partageant cette session du fournisseur d'identité restent également connectées. Une déconnexion correcte comprend trois parties : effacer la session locale de votre application, envoyer le navigateur vers l'end_session_endpoint du fournisseur (déconnexion initiée par le RP) pour mettre fin à la session du fournisseur, et permettre au fournisseur d'informer les autres applications via la déconnexion back-channel. Avec Casdoor, l'endpoint de fin de session est /api/logout, la déconnexion back-channel est configurée par application, et les jetons de rafraîchissement peuvent être révoqués via /api/login/oauth/revoke.

Mécanismes de déconnexion en un coup d'œil​

MécanismeQui le déclencheCe qu'il effacePrise en charge par Casdoor
Déconnexion locale de l'appVotre appLe cookie de session de votre app et les jetons stockés uniquementVotre code
Déconnexion initiée par le RPVotre app, en redirigeant le navigateurLa session navigateur Casdoor et le jeton de l'id_token_hintOui, GET ou POST /api/logout, annoncé comme end_session_endpoint
Déconnexion back-channelCasdoor, de serveur à serveurLes sessions des autres apps, chaque app termine la sienneOui, par application Backchannel logout URL
Déconnexion front-channelLe fournisseur, via des iframes cachéesLes sessions des autres apps, via le navigateurNon, Casdoor n'annonce pas frontchannel_logout_supported
Révocation de jeton (RFC 7009)Votre app, serveur ou clientUn jeton d'accès ou de rafraîchissement, et les jetons partageant son jeton de rafraîchissementOui, POST /api/login/oauth/revoke, annoncé comme revocation_endpoint
Déconnexion SSO CasdoorVotre app, avec le jeton ou le cookie de l'utilisateurToutes les sessions et tous les jetons de l'utilisateur, ou seulement la session actuelleOui, /api/sso-logout, spécifique à Casdoor
Déconnexion unique SAMLCasdoor ou un fournisseur de services SAMLSessions dans les apps SAMLOui, LogoutRequest signé

Session locale vs. session du fournisseur d'identité​

Il y a au moins deux sessions impliquées dans chaque connexion OIDC, et souvent trois :

  1. La session de votre app. Généralement un cookie qui associe l'utilisateur aux jetons obtenus auprès de l'endpoint de jeton.
  2. La session Casdoor. Un cookie sur le domaine Casdoor. Tant qu'il est valide, Casdoor connecte l'utilisateur à toute application de l'organisation sans mot de passe, après un sélecteur de compte en un clic ou silencieusement avec la connexion automatique. C'est ce qui permet le fonctionnement de l'authentification unique.
  3. Une session du fournisseur d'identité amont, lorsque l'utilisateur s'est connecté à Casdoor via Google, GitHub, un IdP d'entreprise, etc.

Auth0 décrit les trois mêmes couches dans sa documentation sur la déconnexion (couches de session application, Auth0 et fournisseur d'identité), et ce modèle s'applique à tout fournisseur OIDC.

« La terminaison localisée de la session applicative » ne met fin qu'à la première : votre application oublie l'utilisateur, mais la session Casdoor continue. C'est ce qui se passe lorsque vous effacez simplement un cookie. « La déconnexion unique centralisée » met fin à la session Casdoor et indique à chaque application de l'utilisateur de mettre fin à sa propre session. La plupart des utilisateurs qui cliquent sur Sign out s'attendent à la seconde option, surtout sur un ordinateur partagé.

Pour la troisième couche, Casdoor appelle l'URL de déconnexion d'un fournisseur OAuth personnalisé lorsque l'application connecte les utilisateurs via celui-ci et que le fournisseur a une URL de déconnexion configurée. Cela ne déconnecte pas l'utilisateur de Google ou GitHub.

Déconnexion initiée par le RP avec le point de terminaison end-session​

OpenID Connect RP-Initiated Logout 1.0 définit comment une partie prenante (votre application) demande au fournisseur de mettre fin à sa session. Le fournisseur publie le point de terminaison sous end_session_endpoint dans son document de découverte, et la requête contient ces paramètres :

  • id_token_hint : le jeton ID émis par le fournisseur à votre application. La spécification le marque comme RECOMMENDED. Casdoor l'utilise pour trouver et expirer ce jeton exact.
  • post_logout_redirect_uri : où le navigateur va ensuite. La spécification exige qu'il soit enregistré à l'avance. Casdoor le vérifie par rapport aux Redirect URLs de l'application et refuse toute redirection vers autre chose.
  • state : une valeur opaque que Casdoor ajoute à la redirection, afin que votre application puisse distinguer son propre aller-retour de déconnexion d'une tentative forgée.
  • client_id : identifie l'application lorsque vous ne pouvez pas envoyer de jeton ID.

La spécification définit également logout_hint et ui_locales. Casdoor ne les lit pas.

Le document de découverte de Casdoor situé à /.well-known/openid-configuration annonce end_session_endpoint comme étant https://<your-casdoor>/api/logout, de sorte que les bibliothèques OIDC prenant en charge la déconnexion initiée par le RP le reprennent sans configuration. Le point de terminaison accepte GET et POST, comme l'exige la spécification.

Ce que Casdoor fait avec la requête, brièvement (la référence de déconnexion initiée par le RP détaille chaque branche) :

  • Avec id_token_hint, Casdoor recherche le jeton, envoie des jetons de déconnexion back-channel aux applications de l'utilisateur, expire le jeton (son jeton de rafraîchissement cesse également de fonctionner), efface la session Casdoor du navigateur et redirige vers post_logout_redirect_uri avec state.
  • Sans id_token_hint, Casdoor déconnecte la session du navigateur actuel et expire les jetons émis dans celle-ci. Si le navigateur n'a plus de session, il redirige toujours s'il connaît l'application, grâce à la session ou à client_id.
  • Sans post_logout_redirect_uri, le point de terminaison renvoie une réponse JSON au lieu d'une redirection.

Exemple : bouton de déconnexion dans une application web​

Une application monopage (SPA) qui s'est connectée via le flux de code d'autorisation avec PKCE conserve le jeton ID après l'échange de jetons. Le gestionnaire de déconnexion efface d'abord l'état local, puis transmet le navigateur à 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");

Ajoutez https://app.example.com/signed-out aux Redirect URLs de l'application dans Casdoor, sinon Casdoor renverra une erreur au lieu de rediriger. Une application rendue côté serveur procède de même depuis sa route de déconnexion : détruisez la session serveur, puis répondez avec une redirection 302 vers l'URL de fin de session. Pour Spring Security, consultez l'article SSO Spring Boot.

Déconnexion back-channel : informer les autres applications​

Terminer la session Casdoor empêche les nouvelles connexions silencieuses, mais l'application B possède toujours son propre cookie de session datant d'une heure. Quelque chose doit informer l'application B. OpenID Connect Back-Channel Logout 1.0 effectue cela de serveur à serveur : le fournisseur envoie une requête POST avec un paramêtre de formulaire logout_token, un JWT signé nommant l'utilisateur, à l'URI back-channel enregistrée de chaque application.

Dans Casdoor, configurez Backchannel logout URL sur la page d'édition de l'application. Lorsqu'un utilisateur se déconnecte via /api/logout, Casdoor envoie un jeton de déconnexion à chaque application qui détient un jeton actif pour cet utilisateur et possède cette URL configurée. Le jeton est signé avec le certificat de l'application, la même clé que ses jetons ID, et contient :

  • iss et aud : le même émetteur et identifiant client que les jetons ID de l'application
  • sub : l'ID de l'utilisateur, la même valeur que sub dans le jeton ID
  • iat et jti
  • events avec le membre http://schemas.openid.net/event/backchannel-logout
  • sid uniquement pour certaines déconnexions, telles que la déconnexion SSO, et non pour /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.

Exemple : un récepteur de déconnexion back-channel​

Ce gestionnaire Express suit les étapes de validation de la spécification, en utilisant la bibliothèque jose :

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 vérifie la signature, iss, aud, et avec maxTokenAge la revendication iat. Pour retrouver les sessions par utilisateur, stockez sub issu du jeton ID dans chaque enregistrement de session lors de la connexion. Purgez seenJti périodiquement, ou conservez-le dans Redis.

Révocation des jetons de rafraîchissement​

Un jeton de rafraîchissement survit à la session qui l'a créé. Si votre application en conserve un, une déconnexion qui ne le touche jamais laisse une authentification capable de générer de nouveaux jetons d'accès pendant des jours.

L'appel de fin de session avec id_token_hint expire déjà ce jeton, y compris le jeton de rafraîchissement. Révoquez explicitement lorsque la déconnexion ne passe pas par le navigateur, par exemple dans une CLI, une application desktop ou mobile, ou lorsque l'utilisateur quitte uniquement votre application et conserve la session Casdoor. Casdoor implémente RFC 7009 au niveau du revocation_endpoint de son document de découverte :

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

Un client public qui utilise PKCE peut envoyer client_id dans le corps de la requête sans secret. La révocation d'un jeton de rafraîchissement expire également les jetons d'accès émis avec lui. Ensuite, le point de terminaison de rafraîchissement répond invalid_grant et l'inspection retourne "active": false ; Quand Casdoor révoque des jetons liste tous les événements qui déclenchent cette action.

Une limite s'applique à tous les fournisseurs qui émettent des jetons d'accès JWT : une API qui ne vérifie que la signature et exp continue d'accepter un jeton d'accès révoqué jusqu'à son expiration. Maintenez des jetons d'accès de courte durée, ou faites appel à l'inspection de jetons par l'API si elle doit détecter immédiatement une déconnexion.

Déconnexion unique (Single sign-out) entre plusieurs applications​

Avec les éléments ci-dessus, la déconnexion unique fonctionne ainsi lorsque l'utilisateur clique sur "Sign out" dans l'application A :

  1. L'application A efface sa session et redirige vers /api/logout avec id_token_hint.
  2. Casdoor envoie des jetons de déconnexion aux URL de back-channel des autres applications de l'utilisateur, expire le jeton et met fin à la session Casdoor du navigateur.
  3. Les applications B et C reçoivent le jeton de déconnexion et suppriment leurs sessions pour ce sub.
  4. Le navigateur arrive sur la page de déconnecté de l'application A. L'ouverture de l'application B affiche désormais la page de connexion Casdoor.

Une application sans URL de back-channel reste connectée jusqu'à l'expiration de sa propre session ou de son jeton d'accès. Si vous ne pouvez pas ajouter un récepteur à une application, maintenez ses sessions courtes ou faites-lui valider le jeton à chaque requête.

Pour "me déconnecter partout" après la perte d'un ordinateur portable, utilisez le point de terminaison de déconnexion SSO de Casdoor avec logoutAll=true : il expire tous les jetons et supprime toutes les sessions de l'utilisateur, et envoie également des notifications signées via les fournisseurs de notification aux applications qui ne parlent pas OIDC. Les administrateurs peuvent mettre fin à des sessions individuelles sur la page Sessions. Pour les applications SAML, voir Déconnexion unique SAML. Lectures complémentaires : déconnexion unique et déconnexion unique SAML sur www.casdoor.com.

Références​