Logout Iniciado pelo RP e Logout SSO no OpenID Connect: Como o Casdoor Desconecta Usuários
Desconectar um usuário de um aplicativo OpenID Connect exige mais do que apenas excluir seu próprio cookie de sessão. O usuário ainda está conectado ao provedor de identidade, então o próximo clique em "Sign in" passa direto sem senha, e os outros aplicativos que compartilham essa sessão do provedor de identidade permanecem conectados também. Um logout adequado tem três partes: limpar a sessão local do seu aplicativo, enviar o navegador para o end_session_endpoint do provedor (logout iniciado pelo RP) para que a sessão do provedor seja encerrada e permitir que o provedor informe os outros aplicativos através do logout back-channel. No Casdoor, o endpoint de fim de sessão é /api/logout, o logout back-channel é configurado por aplicação e os refresh tokens podem ser revogados em /api/login/oauth/revoke.
Mecanismos de logout em resumo
| Mecanismo | Quem aciona | O que limpa | Suporte no Casdoor |
|---|---|---|---|
| Logout local da aplicação | Sua aplicação | Apenas o cookie de sessão da sua aplicação e os tokens armazenados | Seu código |
| Logout iniciado pelo RP | Sua aplicação, redirecionando o navegador | A sessão do navegador do Casdoor e o token do id_token_hint | Sim, GET ou POST /api/logout, anunciado como end_session_endpoint |
| Logout back-channel | Casdoor, servidor para servidor | As sessões de outros aplicativos; cada aplicativo encerra a própria | Sim, por aplicação Backchannel logout URL |
| Logout front-channel | O provedor, através de iframes ocultos | As sessões de outros aplicativos, através do navegador | Não, o Casdoor não anuncia frontchannel_logout_supported |
| Revogação de token (RFC 7009) | Sua aplicação, servidor ou cliente | Um access token ou refresh token, e os tokens que compartilham seu refresh token | Sim, POST /api/login/oauth/revoke, anunciado como revocation_endpoint |
| Logout SSO do Casdoor | Sua aplicação, com o token ou cookie do usuário | Todas as sessões e tokens do usuário, ou apenas a sessão atual | Sim, /api/sso-logout, específico do Casdoor |
| Logout único SAML | Casdoor ou um service provider SAML | Sessões em aplicativos SAML | Sim, LogoutRequest assinado |
Sessão local vs. sessão do provedor de identidade
Há pelo menos duas sessões envolvidas em cada login OIDC, e frequentemente três:
- A sessão do seu aplicativo. Geralmente um cookie que mapeia para o usuário e os tokens obtidos do endpoint de token.
- A sessão do Casdoor. Um cookie no domínio do Casdoor. Enquanto for válida, o Casdoor autentica o usuário em qualquer aplicativo da organização sem senha, após um seletor de conta de um clique ou silenciosamente com login automático. É isso que faz o single sign-on funcionar.
- Uma sessão de provedor de identidade upstream, quando o usuário fez login no Casdoor através do Google, GitHub, um IdP corporativo e assim por diante.
A Auth0 descreve as mesmas três camadas em sua documentação de logout (camadas de sessão da aplicação, da Auth0 e do provedor de identidade), e o modelo se aplica a qualquer provedor OIDC.
"Terminação localizada da sessão da aplicação" encerra apenas a primeira: seu aplicativo esquece o usuário, mas a sessão do Casdoor permanece ativa. Isso é o que acontece quando você simplesmente limpa um cookie. "Logout centralizado único" encerra a sessão do Casdoor e instrui todos os aplicativos do usuário a encerrarem suas próprias sessões. A maioria dos usuários que clicam em "Sign out" espera a segunda opção, especialmente em um computador compartilhado.
Para a terceira camada, o Casdoor chama a URL de logout de um provedor OAuth personalizado quando a aplicação autentica usuários por meio dele e o provedor tem uma URL de logout definida. Ele não faz logout do usuário no Google ou GitHub.
Logout iniciado pelo RP com o endpoint end-session
OpenID Connect RP-Initiated Logout 1.0 define como uma relying party (seu app) solicita ao provedor que encerre sua sessão. O provedor publica o endpoint como end_session_endpoint em seu documento de descoberta, e a requisição carrega estes parâmetros:
id_token_hint: o ID token emitido pelo provedor para o seu app. A especificação o marca como RECOMENDADO. O Casdoor usa-o para encontrar e expirar esse token exato.post_logout_redirect_uri: para onde o navegador vai em seguida. A especificação exige que ele seja registrado previamente. O Casdoor verifica contra os Redirect URLs da aplicação e recusa redirecionar para qualquer outra coisa.state: um valor opaco que o Casdoor anexa ao redirecionamento, para que seu app possa distinguir seu próprio ciclo de logout de um forjado.client_id: identifica o app quando você não pode enviar um ID token.
A especificação também define logout_hint e ui_locales. O Casdoor não lê esses campos.
O documento de descoberta do Casdoor em /.well-known/openid-configuration anuncia end_session_endpoint como https://<your-casdoor>/api/logout, então bibliotecas OIDC que suportam logout iniciado pelo RP o detectam sem configuração. O endpoint aceita GET e POST, conforme exigido pela especificação.
O que o Casdoor faz com a requisição, brevemente (a referência de logout iniciado pelo RP tem cada ramificação):
- Com
id_token_hint, o Casdoor procura o token, envia tokens de logout back-channel para as aplicações do usuário, expira o token (seu refresh token também deixa de funcionar), limpa a sessão do Casdoor no navegador e redireciona parapost_logout_redirect_uricomstate. - Sem
id_token_hint, o Casdoor faz logout da sessão atual do navegador e expira os tokens emitidos nela. Se o navegador não tiver mais nenhuma sessão, ele ainda redireciona quando conhece o app, através da sessão ou doclient_id. - Sem
post_logout_redirect_uri, o endpoint retorna uma resposta JSON em vez de um redirecionamento.
Exemplo: botão de sign-out em um app de navegador
Uma single-page app que fez login com o fluxo de código de autorização e PKCE mantém o ID token após a troca de tokens. O handler de sign-out limpa primeiro o estado local, depois entrega o navegador ao 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");
Adicione https://app.example.com/signed-out aos Redirect URLs da aplicação no Casdoor, ou o Casdoor retornará um erro em vez de redirecionar. Um app renderizado no servidor faz o mesmo a partir de sua rota de logout: destrua a sessão do servidor, então responda com um 302 para a URL end-session. Para Spring Security, veja o post de SSO Spring Boot.
Back-channel logout: informando os outros apps
Encerrar a sessão do Casdoor impede novos logins silenciosos, mas o app B ainda tem seu próprio cookie de sessão de uma hora atrás. Algo precisa informar o app B. OpenID Connect Back-Channel Logout 1.0 faz isso de servidor para servidor: o provedor envia um POST com um parâmetro de formulário logout_token, um JWT assinado que nomeia o usuário, para a URI back-channel registrada de cada app.
No Casdoor, defina Backchannel logout URL na página de edição da aplicação. Quando um usuário faz logout através de /api/logout, o Casdoor envia um token de logout para toda aplicação que possui um token ativo para esse usuário e tem esta URL definida. O token é assinado com o certificado da aplicação, a mesma chave de seus ID tokens, e contém:
isseaud: o mesmo issuer e client ID dos ID tokens do appsub: o ID do usuário, o mesmo valor desubno ID tokeniatejtieventscom o membrohttp://schemas.openid.net/event/backchannel-logoutsidapenas para alguns logouts, como SSO logout, e não para/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.
Exemplo: um receptor de back-channel logout
Este handler Express segue os passos de validação da especificação, usando a biblioteca 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 verifica a assinatura, iss, aud, e com maxTokenAge a claim iat. Para encontrar sessões por usuário, armazene sub do ID token em cada registro de sessão no momento do login. Poda seenJti periodicamente, ou mantenha-o no Redis.
Revogando refresh tokens
Um refresh token sobrevive à sessão que o criou. Se seu app mantém um, um logout que nunca o toca deixa uma credencial que pode gerar novos access tokens por dias.
A chamada end-session com id_token_hint já expira esse token, incluindo o refresh token. Revogue explicitamente quando o logout não passa pelo navegador, por exemplo em um CLI, um app desktop ou móvel, ou quando o usuário sai apenas do seu app e mantém a sessão do Casdoor. O Casdoor implementa RFC 7009 no revocation_endpoint de seu documento de descoberta:
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
Um cliente público que usa PKCE pode enviar client_id no corpo sem um segredo. Revogar um token de atualização também expira os tokens de acesso emitidos com ele. Depois disso, o endpoint de atualização responde invalid_grant e a introspecção retorna "active": false; When Casdoor revokes tokens lista todos os eventos que fazem isso.
Uma limitação se aplica a todos os provedores que emitem tokens de acesso JWT: uma API que verifica apenas a assinatura e exp continua aceitando um token de acesso revogado até que ele expire. Mantenha os tokens de acesso com vida curta ou faça a API chamar a introspecção de token quando precisar notar um logout imediatamente.
Single sign-out entre vários aplicativos
Com os componentes acima, o single sign-out funciona assim quando o usuário clica em "Sign out" no aplicativo A:
- O aplicativo A limpa sua sessão e redireciona para
/api/logoutcomid_token_hint. - O Casdoor envia tokens de logout para as URLs de back-channel dos outros aplicativos do usuário, expira o token e encerra a sessão do navegador no Casdoor.
- Os aplicativos B e C recebem o token de logout e excluem suas sessões para aquele
sub. - O navegador chega na página de deslogado do aplicativo A. Abrir o aplicativo B agora mostra a página de login do Casdoor.
Um aplicativo sem uma URL de back-channel permanece conectado até que sua própria sessão ou token de acesso expire. Se você não puder adicionar um receptor a um aplicativo, mantenha suas sessões curtas ou faça-o validar o token em cada requisição.
Para "deslogar-me em todos os lugares" após um laptop perdido, use o SSO logout endpoint do Casdoor com logoutAll=true: ele expira todos os tokens e exclui todas as sessões do usuário, além de enviar notificações assinadas por meio de provedores de notificação para aplicativos que não falam OIDC. Administradores podem encerrar sessões únicas na Sessions page. Para aplicativos SAML, veja SAML single logout. Leitura complementar: single logout e SAML single logout em www.casdoor.com.