feat(auth): Keycloak RP-initiated 단일 로그아웃(SLO) 적용

feat/sso 작업을 라우트 개편된 현재 main 에 이식.
- handleCallback 이 id_token 반환, 콜백에서 분리해 session.idToken 에만 보관(뷰/DB 비노출)
- logoutUrl(): end_session_endpoint + id_token_hint 로 확인화면 없이 즉시 로그아웃
- /logout: 로컬 세션 destroy 후 Keycloak end-session 으로 리다이렉트해 SSO 세션까지 종료

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0310700
2026-06-30 14:12:42 +09:00
parent 2b74b59f81
commit 096f7fbbcb
2 changed files with 26 additions and 4 deletions

View File

@@ -56,9 +56,22 @@ export async function handleCallback(req) {
email: claims.email,
roles,
isAdmin: roles.includes(config.adminRole),
// SSO 로그아웃(end-session) 시 id_token_hint 로 사용
id_token: tokenSet.id_token,
};
}
// SSO 로그아웃 — Keycloak end_session_endpoint 로 보내 로컬뿐 아니라
// Keycloak SSO 세션까지 종료한다. id_token_hint 가 있으면 확인 화면 없이 즉시 로그아웃.
// 종료 후 post_logout_redirect_uri 로 복귀.
export function logoutUrl(idToken) {
if (!client) return `${config.baseUrl}/login`;
return client.endSessionUrl({
id_token_hint: idToken,
post_logout_redirect_uri: `${config.baseUrl}/login`,
});
}
// 로그인 필수 가드
export function requireAuth(req, res, next) {
if (req.session.user) return next();

View File

@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
import path from "path";
import { config } from "./config.js";
import { toolsForUser } from "./sites.js";
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady } from "./auth.js";
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady, logoutUrl } from "./auth.js";
import { icon, identicon, langColor, langFromTags } from "./helpers.js";
import * as db from "./db.js";
@@ -58,8 +58,11 @@ app.get("/auth/login", (req, res) => loginRedirect(req, res));
app.get("/auth/callback", async (req, res) => {
try {
const u = await handleCallback(req);
await db.upsertUser(u);
req.session.user = u; // { sabun, name, email, roles, isAdmin }
// id_token 은 SSO 로그아웃용으로만 세션에 따로 보관(뷰/DB 에 노출 안 함)
const { id_token, ...user } = u;
req.session.idToken = id_token;
await db.upsertUser(user);
req.session.user = user; // { sabun, name, email, roles, isAdmin }
res.redirect("/");
} catch (e) {
console.error("oidc callback error", e);
@@ -67,7 +70,13 @@ app.get("/auth/callback", async (req, res) => {
}
});
app.get("/logout", (req, res) => {
req.session.destroy(() => res.redirect("/login"));
const idToken = req.session.idToken;
// 로컬 세션을 먼저 destroy 한 뒤 Keycloak end-session 으로 보내
// SSO 세션까지 종료한다. (로컬만 지우면 재접속 시 자동 재로그인됨)
req.session.destroy(() => {
if (oidcReady()) return res.redirect(logoutUrl(idToken));
return res.redirect("/login");
});
});
// --- 대시보드 ---