diff --git a/src/auth.js b/src/auth.js index 51f2e0d..0a643df 100644 --- a/src/auth.js +++ b/src/auth.js @@ -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(); diff --git a/src/server.js b/src/server.js index c70c35b..93a0106 100644 --- a/src/server.js +++ b/src/server.js @@ -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"); + }); }); // --- 대시보드 ---