미인증 진입 시 중간 화면 없이 곧장 Keycloak 로그인 폼으로 리다이렉트

- requireAuth: /login 거치지 않고 loginRedirect로 직접 Keycloak으로
- /login 라우트도 oidcReady면 즉시 리다이렉트
- OIDC discovery 실패 시에만 안내 페이지 폴백, /api/*는 401 유지

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:29:58 +09:00
parent 792b9b9c72
commit 35d5d70a1d
2 changed files with 17 additions and 4 deletions

View File

@@ -4,6 +4,11 @@ import { config } from "./config.js";
let client; let client;
// OIDC 클라이언트가 준비됐는지(discovery 성공) 여부
export function oidcReady() {
return !!client;
}
export async function initOidc() { export async function initOidc() {
const issuer = await Issuer.discover(config.oidc.issuer); const issuer = await Issuer.discover(config.oidc.issuer);
client = new issuer.Client({ client = new issuer.Client({
@@ -47,7 +52,10 @@ export async function handleCallback(req) {
// 로그인 필수 가드 // 로그인 필수 가드
export function requireAuth(req, res, next) { export function requireAuth(req, res, next) {
if (req.session.user) return next(); if (req.session.user) return next();
// API 요청은 401, 페이지 요청은 로그인으로 // API 요청은 401
if (req.path.startsWith("/api/")) return res.status(401).json({ error: "unauthorized" }); if (req.path.startsWith("/api/")) return res.status(401).json({ error: "unauthorized" });
return res.redirect("/login"); // 페이지 요청은 중간 화면 없이 곧장 Keycloak 로그인 폼으로 보낸다.
if (oidcReady()) return loginRedirect(req, res);
// OIDC 미준비(discovery 실패)면 안내 페이지로 폴백
return res.redirect("/login?error=" + encodeURIComponent("로그인 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해 주세요."));
} }

View File

@@ -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 { siteGroups } from "./sites.js"; import { siteGroups } from "./sites.js";
import { initOidc, loginRedirect, handleCallback, requireAuth } from "./auth.js"; import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady } from "./auth.js";
import * as db from "./db.js"; import * as db from "./db.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -36,9 +36,14 @@ app.use((req, res, next) => {
app.get("/healthz", (_req, res) => res.json({ ok: true })); app.get("/healthz", (_req, res) => res.json({ ok: true }));
// --- 인증 --- // --- 인증 ---
// SSO 세션이 없으면 중간 페이지 없이 바로 Keycloak(커스텀 테마) 로그인으로 보낸다.
// (OIDC 초기화 실패 등으로 로그인 불가하면 안내용 login.ejs 로 폴백)
app.get("/login", (req, res) => { app.get("/login", (req, res) => {
if (req.session.user) return res.redirect("/"); if (req.session.user) return res.redirect("/");
res.render("login"); if (req.query.error) return res.render("login", { error: req.query.error });
// OIDC 준비됐으면 중간 화면 없이 곧장 Keycloak 로그인 폼으로
if (oidcReady()) return loginRedirect(req, res);
return res.render("login", { error: "로그인 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해 주세요." });
}); });
app.get("/auth/login", (req, res) => loginRedirect(req, res)); app.get("/auth/login", (req, res) => loginRedirect(req, res));
app.get("/auth/callback", async (req, res) => { app.get("/auth/callback", async (req, res) => {