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:
13
src/auth.js
13
src/auth.js
@@ -56,9 +56,22 @@ export async function handleCallback(req) {
|
|||||||
email: claims.email,
|
email: claims.email,
|
||||||
roles,
|
roles,
|
||||||
isAdmin: roles.includes(config.adminRole),
|
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) {
|
export function requireAuth(req, res, next) {
|
||||||
if (req.session.user) return next();
|
if (req.session.user) return next();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
import { toolsForUser } from "./sites.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 { icon, identicon, langColor, langFromTags } from "./helpers.js";
|
||||||
import * as db from "./db.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) => {
|
app.get("/auth/callback", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const u = await handleCallback(req);
|
const u = await handleCallback(req);
|
||||||
await db.upsertUser(u);
|
// id_token 은 SSO 로그아웃용으로만 세션에 따로 보관(뷰/DB 에 노출 안 함)
|
||||||
req.session.user = u; // { sabun, name, email, roles, isAdmin }
|
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("/");
|
res.redirect("/");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("oidc callback error", e);
|
console.error("oidc callback error", e);
|
||||||
@@ -67,7 +70,13 @@ app.get("/auth/callback", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.get("/logout", (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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 대시보드 ---
|
// --- 대시보드 ---
|
||||||
|
|||||||
Reference in New Issue
Block a user