RP-Initiated Logout та SSO Logout в OpenID Connect: Як Casdoor виходить із системи користувачів
Для виходу користувача з додатку OpenID Connect недостатньо просто видалити власну сесійну куку. Користувач залишається авторизованим у провайдері ідентифікації, тому наступне натискання "Sign in" пройде без введення пароля, а інші додатки, які спільно використовують цю сесію провайдера ідентифікації, також залишаться авторизованими. Повноцінний вихід складається з трьох частин: очистка локальної сесії вашого додатку, перенаправлення браузера на end_session_endpoint провайдера (RP-initiated logout), щоб завершити сесію провайдера, та отримання повідомлень від провайдера іншими додатками через back-channel logout. У Casdoor ендпоінт завершення сесії — це /api/logout, back-channel logout налаштовується для кожного додатку окремо, а refresh токени можна відкликати через /api/login/oauth/revoke.
Механізми виходу оглядом
| Механізм | Хто ініціює | Що очищується | Підтримка Casdoor |
|---|---|---|---|
| Локальний вихід з додатку | Ваш додаток | Тільки сесійна кука вашого додатку та збережені токени | Ваш код |
| RP-initiated logout | Ваш додаток, шляхом перенаправлення браузера | Сесія браузера Casdoor та токен id_token_hint | Так, GET або POST /api/logout, оголошено як end_session_endpoint |
| Back-channel logout | Casdoor, між серверами | Сесії інших додатків, кожен додаток завершує свою власну | Так, для кожного додатку Backchannel logout URL |
| Front-channel logout | Провайдер, через приховані iframe | Сесії інших додатків, через браузер | Ні, Casdoor не оголошує frontchannel_logout_supported |
| Відкликання токенів (RFC 7009) | Ваш додаток, сервер або клієнт | Один access або refresh токен та токени, які мають той самий refresh token | Так, POST /api/login/oauth/revoke, оголошено як revocation_endpoint |
| Вихід з SSO Casdoor | Ваш додаток, з токеном або кукою користувача | Усі сесії та токени користувача, або лише поточна сесія | Так, /api/sso-logout, специфічно для Casdoor |
| Одноразовий вихід SAML | Casdoor або постачальник послуг SAML | Сесії в додатках SAML | Так, підписаний LogoutRequest |
Локальна сесія проти сесії провайдера ідентифікації
У кожному вході OIDC бере участь щонайменше дві сесії, часто три:
- Сесія вашого додатку. Зазвичай це кука, яка відповідає користувачу та токенам, отриманим від ендпоінту токенів.
- Сесія Casdoor. Кука на домені Casdoor. Поки вона дійсна, Casdoor авторизує користувача в будь-якому додатку організації без пароля, після одного кліку у виборі акаунта або автоматично, якщо увімкнено автоматичний вхід. Саме це забезпечує роботу єдиного входу.
- Сесія провайдера ідентифікації верхнього рівня, коли користувач увійшов у Casdoor через Google, GitHub, корпоративний IdP тощо.
Auth0 описує ті самі три рівні у своїй документації щодо виходу (рівні сесій додатка, Auth0 та провайдера ідентифікації), і ця модель застосовна до будь-якого OIDC-провайдера.
"Локальне завершення сесії додатка" завершує лише перший рівень: ваш додаток забуває користувача, але сесія Casdoor залишається активною. Це відбувається, коли ви просто очищуєте cookie. "Централізований єдиний вихід" завершує сесію Casdoor і повідомляє кожному додатку користувача завершити власну сесію. Більшість користувачів, натискаючи "Sign out", очікують саме другого варіанту, особливо на спільному комп'ютері.
Для третього рівня Casdoor викликає URL виходу Custom OAuth провайдера, коли додаток входить через нього і якщо для провайдера налаштовано URL виходу. Він не виводить користувача з Google або GitHub.
RP-initiated logout із використанням end-session endpoint
OpenID Connect RP-Initiated Logout 1.0 визначає, як relying party (ваш додаток) просить провайдера завершити сесію. Провайдер публікує endpoint як end_session_endpoint у своєму discovery document, а запит містить такі параметри:
id_token_hint: ID token, який провайдер видав вашому додатку. У специфікації він позначений як RECOMMENDED. Casdoor використовує його для пошуку та термінування саме цього токена.post_logout_redirect_uri: куди браузер переходить після цього. Специфікація вимагає попередньої реєстрації цього URI. Casdoor перевіряє його відповідність списку Redirect URLs додатка і відмовляється перенаправляти на інші адреси.state: непрозоре значення, яке Casdoor додає до перенаправлення, щоб ваш додаток міг відрізнити власний цикл виходу від підробленого.client_id: ідентифікує додаток, коли ви не можете надіслати ID token.
Специфікація також визначає logout_hint та ui_locales. Casdoor їх не читає.
Discovery document Casdoor за адресою /.well-known/openid-configuration оголошує end_session_endpoint як https://<your-casdoor>/api/logout, тому OIDC бібліотеки, що підтримують RP-initiated logout, виявляють його без додаткового налаштування. Endpoint приймає запити GET та POST, як це передбачено специфікацією.
Коротко про те, що Casdoor робить із запитом (детальніше в RP-initiated logout reference):
- З
id_token_hintCasdoor шукає токен, надсилає back-channel logout tokens додаткам користувача, термінує токен (його refresh token також стає недійсним), очищує сесію Casdoor у браузері та перенаправляє наpost_logout_redirect_uriз параметромstate. - Без
id_token_hintCasdoor виводить поточну браузерну сесію та термінує видані в ній токени. Якщо в браузері не залишилося сесій, він все одно перенаправляє, якщо знає додаток зі сесії або зclient_id. - Без
post_logout_redirect_uriendpoint повертає JSON-відповідь замість перенаправлення.
Приклад: кнопка виходу в браузерному додатку
Single-page app, який увійшов через authorization code flow з PKCE, зберігає ID token після обміну токенів. Обробник виходу спочатку очищує локальний стан, а потім передає браузер 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");
Додайте https://app.example.com/signed-out до Redirect URLs додатка в Casdoor, інакше Casdoor поверне помилку замість перенаправлення. Серверний рендеринговий додаток робить те саме зі свого маршруту виходу: знищує серверну сесію, а потім відповідає 302 редиректом на end-session URL. Для Spring Security див. Spring Boot SSO post.
Back-channel logout: повідомлення інших додатків
Завершення сесії Casdoor зупиняє нові тихі входи (silent sign-ins), але додаток B ще має свою сесійну cookie, створену годину тому. Щось має повідомити додатку B. OpenID Connect Back-Channel Logout 1.0 робить це між серверами: провайдер надсилає POST з форм-параметром logout_token — підписаним JWT, що містить ім'я користувача — на зареєстрований back-channel URI кожного додатка.
У Casdoor встановіть Backchannel logout URL на сторінці редагування додатка. Коли користувач виходить через /api/logout, Casdoor надсилає logout token кожному додатку, який має активний токен для цього користувача і де налаштовано цей URL. Токен підписаний сертифікатом додатка (тим самим ключем, що й його ID tokens) і містить:
issтаaud: той самий issuer і client ID, що й в ID tokens додаткаsub: ID користувача, те саме значення, що йsubв ID tokeniatтаjtieventsз членомhttp://schemas.openid.net/event/backchannel-logoutsidлише для деяких типів виходу, таких як SSO logout, і не для/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.
Приклад: приймач back-channel logout
Цей Express handler слідує крокам валідації зі специфікації, використовуючи бібліотеку 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 перевіряє підпис, iss, aud, а з опцією maxTokenAge — claim iat. Щоб знаходити сесії за користувачем, зберігайте sub з ID token у записі кожної сесії під час входу. Періодично очищуйте seenJti або зберігайте його в Redis.
Відкликання refresh tokens
Refresh token живе довше, ніж сесія, яка його створила. Якщо ваш додаток зберігає його, вихід, який його не чіпає, залишає облікові дані, які можуть генерувати нові access tokens протягом кількох днів.
Виклик end-session з id_token_hint вже термінує цей токен, включаючи refresh token. Відкликайте явно, коли вихід не проходить через браузер, наприклад, у CLI, десктопному чи мобільному додатку, або коли користувач залишає лише ваш додаток, зберігаючи сесію Casdoor. Casdoor реалізує RFC 7009 на revocation_endpoint у своєму discovery document:
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
Публічний клієнт, що використовував PKCE, може надсилати client_id в тілі запиту без секрету. Відкликання refresh-токена також призводить до закінчення терміну дії виданих із ним access-токенів. Після цього ендпоінт оновлення відповідає помилкою invalid_grant, а інтроспекція повертає "active": false; розділ Коли Casdoor відкликає токени містить перелік усіх подій, які спричиняють таку поведінку.
Одне обмеження стосується всіх провайдерів, які видають JWT access-токени: API, яке перевіряє лише підпис і поле exp, продовжує приймати відкликаний access-токен доти, доки він не закінчиться. Зберігайте короткостроковість access-токенів або змусьте API викликати інтроспекцію токенів, якщо воно має миттєво реагувати на вихід користувача.
Єдиний вихід (single sign-out) для кількох додатків
Завдяки описаним вище механізмам єдиний вихід працює так, коли користувач натискає "Sign out" у додатку A:
- Додаток A очищає свою сесію і перенаправляє на
/api/logoutз параметромid_token_hint. - Casdoor надсилає logout-токени на back-channel URL інших додатків користувача, завершує дію токена та припиняє сесію Casdoor у браузері.
- Додатки B і C отримують logout-токен і видаляють свої сесії для відповідного
sub. - Браузер потрапляє на сторінку виходу додатка A. При відкритті додатка B тепер відображається сторінка входу Casdoor.
Додаток без back-channel URL залишається авторизованим, поки його власна сесія або access-токен не закінчать дію. Якщо ви не можете додати обробник до додатка, скоротіть тривалість його сесій або змусіть його валідувати токен при кожному запиті.
Для функції "вийти скрізь" після втрати ноутбука використовуйте SSO logout endpoint Casdoor з параметром logoutAll=true: це завершує дію всіх токенів і видаляє всі сесії користувача, а також надсилає підписані сповіщення через провайдери нотифікацій додаткам, які не підтримують OIDC. Адміністратори можуть завершувати окремі сесії на сторінці Sessions (Керування сесіями). Для додатків SAML дивіться SAML single logout. Додатково: статті single logout та SAML single logout на www.casdoor.com.