1
0
forked from baron/baron-sso

Baron 통합로그인 -> Baron 로그인 명칭 변경

This commit is contained in:
Lectom C Han
2026-02-05 18:36:28 +09:00
parent d3d9f7bea6
commit 62b5bdba76
13 changed files with 357 additions and 340 deletions

View File

@@ -1,6 +1,6 @@
# Baron SSO # Baron SSO
**Baron 통합로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다. **Baron 로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다.
* Ory Stack으로 모든 구성요소를 self-hosting 합니다. * Ory Stack으로 모든 구성요소를 self-hosting 합니다.
* Backend는 Go (Fiber)로 구성된 Ory Stack의 유일한 Command 전송 포인트입니다. 모든 Command는 ClickHouse로 강제 전송되며 Audit Log 시스템을 구성합니다. * Backend는 Go (Fiber)로 구성된 Ory Stack의 유일한 Command 전송 포인트입니다. 모든 Command는 ClickHouse로 강제 전송되며 Audit Log 시스템을 구성합니다.
@@ -75,7 +75,7 @@ flowchart
### 4. 주요 시나리오 (Core Scenarios) ### 4. 주요 시나리오 (Core Scenarios)
1. **Same Browser SSO**: Baron 통합로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인). 1. **Same Browser SSO**: Baron 로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인).
1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동 1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동
2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화 2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화
2.1 향후 App Push 등 2차 인증 강화수단 검토 필요 2.1 향후 App Push 등 2차 인증 강화수단 검토 필요

View File

@@ -1,142 +1,148 @@
import { import {
BadgeCheck, BadgeCheck,
Building2, Building2,
Key, Key,
KeyRound, KeyRound,
LayoutDashboard, LayoutDashboard,
Moon, Moon,
NotebookTabs, NotebookTabs,
ShieldHalf, ShieldHalf,
Sun, Sun,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NavLink, Outlet } from "react-router-dom"; import { NavLink, Outlet } from "react-router-dom";
import RoleSwitcher from "./RoleSwitcher"; import RoleSwitcher from "./RoleSwitcher";
const navItems = [ const navItems = [
{ label: "Overview", to: "/", icon: LayoutDashboard }, { label: "Overview", to: "/", icon: LayoutDashboard },
{ label: "Tenant Dashboard", to: "/dashboard", icon: ShieldHalf }, { label: "Tenant Dashboard", to: "/dashboard", icon: ShieldHalf },
{ label: "Tenants", to: "/tenants", icon: Building2 }, { label: "Tenants", to: "/tenants", icon: Building2 },
{ label: "Users", to: "/users", icon: Users }, { label: "Users", to: "/users", icon: Users },
{ label: "API Keys (M2M)", to: "/api-keys", icon: Key }, { label: "API Keys (M2M)", to: "/api-keys", icon: Key },
{ label: "Audit Logs", to: "/audit-logs", icon: NotebookTabs }, { label: "Audit Logs", to: "/audit-logs", icon: NotebookTabs },
{ label: "Auth Guard", to: "/auth", icon: KeyRound }, { label: "Auth Guard", to: "/auth", icon: KeyRound },
]; ];
function AppLayout() { function AppLayout() {
const [theme, setTheme] = useState<"light" | "dark">(() => { const [theme, setTheme] = useState<"light" | "dark">(() => {
const stored = window.localStorage.getItem("admin_theme"); const stored = window.localStorage.getItem("admin_theme");
return stored === "dark" ? "dark" : "light"; return stored === "dark" ? "dark" : "light";
}); });
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
root.classList.remove("light", "dark"); root.classList.remove("light", "dark");
if (theme === "light") { if (theme === "light") {
root.classList.add("light"); root.classList.add("light");
} else { } else {
root.classList.add("dark"); root.classList.add("dark");
} }
window.localStorage.setItem("admin_theme", theme); window.localStorage.setItem("admin_theme", theme);
}, [theme]); }, [theme]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light")); setTheme((prev) => (prev === "light" ? "dark" : "light"));
}; };
return ( return (
<div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]"> <div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]">
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur"> <aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6"> <div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
<div className="flex items-center gap-3 md:flex-col md:items-start"> <div className="flex items-center gap-3 md:flex-col md:items-start">
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]"> <div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
<ShieldHalf size={20} /> <ShieldHalf size={20} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">
Admin Control
</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /admin
</div>
</div>
<nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
IDP env: prod
</span>
<span className="rounded-full border border-border px-3 py-1">
Tenant-aware headers
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> /admin .</p>
<p>
IDP API로만 , ·
.
</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Admin Plane
</p>
<span className="text-lg font-semibold">
Tenant isolation & least privilege by default
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? (
<Sun size={16} />
) : (
<Moon size={16} />
)}
{theme === "light" ? "Light" : "Dark"}
</button>
<span className="rounded-full border border-border px-3 py-2 text-muted-foreground">
Session TTL: 15m admin
</span>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
<RoleSwitcher />
</div> </div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">Admin Control</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /admin
</div>
</div> </div>
<nav className="px-2 pb-4 md:px-3 md:pb-8"> );
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
IDP env: prod
</span>
<span className="rounded-full border border-border px-3 py-1">
Tenant-aware headers
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> /admin .</p>
<p>
IDP API로만 , ·
.
</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Admin Plane
</p>
<span className="text-lg font-semibold">
Tenant isolation & least privilege by default
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? <Sun size={16} /> : <Moon size={16} />}
{theme === "light" ? "Light" : "Dark"}
</button>
<span className="rounded-full border border-border px-3 py-2 text-muted-foreground">
Session TTL: 15m admin
</span>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
<RoleSwitcher />
</div>
</div>
);
} }
export default AppLayout; export default AppLayout;

View File

@@ -237,7 +237,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
} }
subject := "[Baron 통합로그인] 회원가입 인증코드" subject := "[Baron 로그인] 회원가입 인증코드"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="padding: 20px; font-family: sans-serif;"> <div style="padding: 20px; font-family: sans-serif;">
<h2>이메일 인증</h2> <h2>이메일 인증</h2>
@@ -287,7 +287,7 @@ func (h *AuthHandler) SendSignupSmsCode(c *fiber.Ctx) error {
h.saveSignupState(key, newState, signupStateExpiration) h.saveSignupState(key, newState, signupStateExpiration)
// 4. Send SMS // 4. Send SMS
content := fmt.Sprintf("[Baron 통합로그인] 인증번호 [%s]를 입력해주세요.", code) content := fmt.Sprintf("[Baron 로그인] 인증번호 [%s]를 입력해주세요.", code)
go h.SmsService.SendSms(phone, content) go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "Verification code sent"}) return c.JSON(fiber.Map{"message": "Verification code sent"})
@@ -808,7 +808,7 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000)) code := fmt.Sprintf("%06d", rand.Intn(1000000))
content := fmt.Sprintf("[Baron 통합로그인] 인증번호: %s", code) content := fmt.Sprintf("[Baron 로그인] 인증번호: %s", code)
h.RedisService.StoreVerificationCode(sanitizedPhone, code) h.RedisService.StoreVerificationCode(sanitizedPhone, code)
if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil { if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil {
@@ -991,10 +991,10 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
} }
subject := "[Baron 통합로그인] 링크" subject := "[Baron 로그인] 링크"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;"> <div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;">
<h2 style="color: #1A1F2C;">Baron SSO 로그인</h2> <h2 style="color: #1A1F2C;">Baron 로그인</h2>
<p>안녕하세요,</p> <p>안녕하세요,</p>
<p>아래 버튼을 클릭하여 로그인을 완료해 주세요. 이 링크는 5분 동안 유효합니다.</p> <p>아래 버튼을 클릭하여 로그인을 완료해 주세요. 이 링크는 5분 동안 유효합니다.</p>
<div style="margin: 30px 0;"> <div style="margin: 30px 0;">
@@ -1016,7 +1016,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
} }
} else { } else {
// Send SMS // Send SMS
content := fmt.Sprintf("[Baron 통합로그인] 로그인 링크: %s | 코드: %s", link, userCode) content := fmt.Sprintf("[Baron 로그인] 로그인 링크: %s | 코드: %s", link, userCode)
if drySend { if drySend {
slog.Info("[Enchanted][DrySend] SMS send skipped", "loginID", loginID, "content", content) slog.Info("[Enchanted][DrySend] SMS send skipped", "loginID", loginID, "content", content)
} else { } else {
@@ -1677,10 +1677,10 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
ale.Log(slog.LevelError, "Email service not configured") ale.Log(slog.LevelError, "Email service not configured")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
} }
subject := "[Baron 통합로그인] 비밀번호 재설정" subject := "[Baron 로그인] 비밀번호 재설정"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;"> <div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;">
<h2 style="color: #1A1F2C;">Baron SSO 비밀번호 재설정</h2> <h2 style="color: #1A1F2C;">Baron 로그인 비밀번호 재설정</h2>
<p>아래 버튼을 클릭해 비밀번호 재설정을 진행해 주세요. 링크는 15분간 유효합니다.</p> <p>아래 버튼을 클릭해 비밀번호 재설정을 진행해 주세요. 링크는 15분간 유효합니다.</p>
<div style="margin: 30px 0;"> <div style="margin: 30px 0;">
<a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">비밀번호 재설정</a> <a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">비밀번호 재설정</a>
@@ -1700,7 +1700,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
} }
} }
} else { } else {
resetSms := fmt.Sprintf("[Baron 통합로그인] 비밀번호 재설정 링크: %s", resetLink) resetSms := fmt.Sprintf("[Baron 로그인] 비밀번호 재설정 링크: %s", resetLink)
if drySend { if drySend {
ale.Log(slog.LevelInfo, "SMS send skipped (dry-send)", slog.String("loginId", loginID), slog.String("content", resetSms)) ale.Log(slog.LevelInfo, "SMS send skipped (dry-send)", slog.String("loginId", loginID), slog.String("content", resetSms))
} else { } else {
@@ -1736,7 +1736,7 @@ func (h *AuthHandler) VerifyPasswordResetPage(c *fiber.Ctx) error {
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Baron SSO - 비밀번호 재설정</title> <title>Baron 로그인 - 비밀번호 재설정</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f5f5; } body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f5f5; }
@@ -2227,7 +2227,7 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
body := strings.TrimSpace(req.Body) body := strings.TrimSpace(req.Body)
if body != "" || subject != "" { if body != "" || subject != "" {
if subject == "" { if subject == "" {
subject = "[Baron 통합로그인] 알림" subject = "[Baron 로그인] 알림"
} }
return subject, body return subject, body
} }
@@ -2251,9 +2251,9 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
if subject == "" { if subject == "" {
if label == "알림" { if label == "알림" {
subject = "[Baron 통합로그인] 알림" subject = "[Baron 로그인] 알림"
} else { } else {
subject = fmt.Sprintf("[Baron 통합로그인] %s 코드", label) subject = fmt.Sprintf("[Baron 로그인] %s 코드", label)
} }
} }
@@ -2272,10 +2272,10 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
} }
if code == "" { if code == "" {
return subject, fmt.Sprintf("[Baron 통합로그인] %s 요청이 도착했습니다", label) return subject, fmt.Sprintf("[Baron 로그인] %s 요청이 도착했습니다", label)
} }
message := fmt.Sprintf("[Baron 통합로그인] %s 코드: %s", label, code) message := fmt.Sprintf("[Baron 로그인] %s 코드: %s", label, code)
if label == "로그인" { if label == "로그인" {
baseURL := os.Getenv("USERFRONT_URL") baseURL := os.Getenv("USERFRONT_URL")
if baseURL == "" { if baseURL == "" {
@@ -2320,9 +2320,9 @@ func (h *AuthHandler) buildKratosShortSmsBody(req *kratosCourierRequest, loginID
return "" return ""
} }
if h.isSmsCodeOnly(loginID) { if h.isSmsCodeOnly(loginID) {
return fmt.Sprintf("[Baron 통합로그인] 로그인 코드: %s", shortCode) return fmt.Sprintf("[Baron 로그인] 로그인 코드: %s", shortCode)
} }
return fmt.Sprintf("[Baron 통합로그인] %s", link) return fmt.Sprintf("[Baron 로그인] %s", link)
} }
func (h *AuthHandler) buildKratosShortEmailBody(req *kratosCourierRequest, loginID string) (string, string) { func (h *AuthHandler) buildKratosShortEmailBody(req *kratosCourierRequest, loginID string) (string, string) {
@@ -2330,10 +2330,10 @@ func (h *AuthHandler) buildKratosShortEmailBody(req *kratosCourierRequest, login
if !ok { if !ok {
return "", "" return "", ""
} }
subject := "[Baron 통합로그인] 로그인 링크" subject := "[Baron 로그인] 로그인 링크"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;"> <div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;">
<h2 style="color: #1A1F2C;">Baron SSO 로그인</h2> <h2 style="color: #1A1F2C;">Baron 로그인</h2>
<p>아래 버튼을 클릭하여 로그인을 완료해 주세요.</p> <p>아래 버튼을 클릭하여 로그인을 완료해 주세요.</p>
<div style="margin: 24px 0;"> <div style="margin: 24px 0;">
<a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">로그인 완료하기</a> <a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">로그인 완료하기</a>
@@ -3074,7 +3074,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs)) items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs))
for i := range authLogs { for i := range authLogs {
log := authLogs[i] log := authLogs[i]
appName := "Baron 통합로그인" appName := "Baron 로그인"
clientID := "" clientID := ""
path := strings.ToLower(extractAuditPath(log)) path := strings.ToLower(extractAuditPath(log))
if strings.Contains(path, "/api/v1/auth/oidc/login/accept") { if strings.Contains(path, "/api/v1/auth/oidc/login/accept") {
@@ -3221,8 +3221,7 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
} }
var sessions []domain.HydraConsentSession var sessions []domain.HydraConsentSession
var lastErr error var lastErr error
hasSuccess := false hasSuccess := false
@@ -3361,14 +3360,14 @@ func (h *AuthHandler) RevokeLinkedRp(c *fiber.Ctx) error {
detailsBytes, _ := json.Marshal(detailsMap) detailsBytes, _ := json.Marshal(detailsMap)
_ = h.AuditRepo.Create(&domain.AuditLog{ _ = h.AuditRepo.Create(&domain.AuditLog{
EventID: GenerateSecureToken(16), EventID: GenerateSecureToken(16),
Timestamp: time.Now(), Timestamp: time.Now(),
UserID: subject, UserID: subject,
EventType: "consent.revoked", EventType: "consent.revoked",
Status: "success", Status: "success",
IPAddress: c.IP(), IPAddress: c.IP(),
UserAgent: string(c.Request().Header.UserAgent()), UserAgent: string(c.Request().Header.UserAgent()),
Details: string(detailsBytes), Details: string(detailsBytes),
}) })
} }
@@ -3534,14 +3533,14 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
detailsBytes, _ := json.Marshal(detailsMap) detailsBytes, _ := json.Marshal(detailsMap)
_ = h.AuditRepo.Create(&domain.AuditLog{ _ = h.AuditRepo.Create(&domain.AuditLog{
EventID: GenerateSecureToken(16), EventID: GenerateSecureToken(16),
Timestamp: time.Now(), Timestamp: time.Now(),
UserID: consentRequest.Subject, UserID: consentRequest.Subject,
EventType: "consent.granted", EventType: "consent.granted",
Status: "success", Status: "success",
IPAddress: c.IP(), IPAddress: c.IP(),
UserAgent: string(c.Request().Header.UserAgent()), UserAgent: string(c.Request().Header.UserAgent()),
Details: string(detailsBytes), Details: string(detailsBytes),
}) })
} }
@@ -3572,7 +3571,6 @@ func (h *AuthHandler) RejectConsentRequest(c *fiber.Ctx) error {
return c.JSON(rejectResp) return c.JSON(rejectResp)
} }
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
var req struct { var req struct {
LoginChallenge string `json:"login_challenge"` LoginChallenge string `json:"login_challenge"`
@@ -4973,7 +4971,7 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
h.RedisService.Set(key, code, 5*time.Minute) h.RedisService.Set(key, code, 5*time.Minute)
// Send SMS // Send SMS
content := fmt.Sprintf("[Baron 통합로그인] 정보 수정 인증번호: [%s]", code) content := fmt.Sprintf("[Baron 로그인] 정보 수정 인증번호: [%s]", code)
go h.SmsService.SendSms(phone, content) go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "인증번호가 전송되었습니다."}) return c.JSON(fiber.Map{"message": "인증번호가 전송되었습니다."})

View File

@@ -6,109 +6,118 @@ import { Toaster } from "../ui/toaster";
const navItems = [{ label: "Clients", to: "/clients", icon: ShieldHalf }]; const navItems = [{ label: "Clients", to: "/clients", icon: ShieldHalf }];
function AppLayout() { function AppLayout() {
const [theme, setTheme] = useState<"light" | "dark">(() => { const [theme, setTheme] = useState<"light" | "dark">(() => {
const stored = window.localStorage.getItem("admin_theme"); const stored = window.localStorage.getItem("admin_theme");
return stored === "dark" ? "dark" : "light"; return stored === "dark" ? "dark" : "light";
}); });
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
root.classList.remove("light", "dark"); root.classList.remove("light", "dark");
if (theme === "light") { if (theme === "light") {
root.classList.add("light"); root.classList.add("light");
} else { } else {
root.classList.add("dark"); root.classList.add("dark");
} }
window.localStorage.setItem("admin_theme", theme); window.localStorage.setItem("admin_theme", theme);
}, [theme]); }, [theme]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light")); setTheme((prev) => (prev === "light" ? "dark" : "light"));
}; };
return ( return (
<div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]"> <div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]">
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur"> <aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6"> <div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
<div className="flex items-center gap-3 md:flex-col md:items-start"> <div className="flex items-center gap-3 md:flex-col md:items-start">
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]"> <div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
<ShieldHalf size={20} /> <ShieldHalf size={20} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">
Developer Console
</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /dev
</div>
</div>
<nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
Env: dev
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> .</p>
<p>
.
</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Dev Plane
</p>
<span className="text-lg font-semibold">
Manage your applications
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? (
<Sun size={16} />
) : (
<Moon size={16} />
)}
{theme === "light" ? "Light" : "Dark"}
</button>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
</div> </div>
<div> <Toaster />
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">Developer Console</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /dev
</div>
</div> </div>
<nav className="px-2 pb-4 md:px-3 md:pb-8"> );
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
Env: dev
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> .</p>
<p> .</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Dev Plane
</p>
<span className="text-lg font-semibold">
Manage your applications
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? <Sun size={16} /> : <Moon size={16} />}
{theme === "light" ? "Light" : "Dark"}
</button>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
</div>
<Toaster />
</div>
);
} }
export default AppLayout; export default AppLayout;

View File

@@ -1,4 +1,4 @@
[Baron 통합로그인] 로그인 링크 [Baron 로그인] 로그인 링크
# 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }} # 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }}
http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }} http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }}
코드: {{ .LoginCode }} 코드: {{ .LoginCode }}

View File

@@ -118,7 +118,7 @@ flowchart
- RP별 Consent 관리 - RP별 Consent 관리
### 5. 주요 시나리오 (Core Scenarios) ### 5. 주요 시나리오 (Core Scenarios)
1. **Same Browser SSO**: Baron 통합로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인). 1. **Same Browser SSO**: Baron 로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인).
1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동 1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동
2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화 2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화
2.1 향후 App Push 등 2차 인증 강화수단 검토 필요 2.1 향후 App Push 등 2차 인증 강화수단 검토 필요

View File

@@ -9,17 +9,19 @@ import json
import sys import sys
from dotenv import load_dotenv from dotenv import load_dotenv
def get_env_variable(key, env_file): def get_env_variable(key, env_file):
"""Reads an environment variable from a given .env file.""" """Reads an environment variable from a given .env file."""
with open(env_file, 'r') as f: with open(env_file, "r") as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line and not line.startswith('#'): if line and not line.startswith("#"):
k, v = line.split('=', 1) k, v = line.split("=", 1)
if k == key: if k == key:
return v.strip() return v.strip()
return None return None
def main(): def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: python test/test_sms.py <recipient_phone_number>") print("Usage: python test/test_sms.py <recipient_phone_number>")
@@ -28,10 +30,10 @@ def main():
recipient_phone = sys.argv[1] recipient_phone = sys.argv[1]
# Load environment variables from .env or .env.sample # Load environment variables from .env or .env.sample
env_path = os.path.join(os.getcwd(), '.env') env_path = os.path.join(os.getcwd(), ".env")
if not os.path.exists(env_path): if not os.path.exists(env_path):
print("Info: .env file not found. Using .env.sample as a fallback.") print("Info: .env file not found. Using .env.sample as a fallback.")
env_path = os.path.join(os.getcwd(), '.env.sample') env_path = os.path.join(os.getcwd(), ".env.sample")
if not os.path.exists(env_path): if not os.path.exists(env_path):
print("Error: No configuration file found (.env or .env.sample).") print("Error: No configuration file found (.env or .env.sample).")
@@ -43,7 +45,9 @@ def main():
sender_phone = get_env_variable("NAVER_SENDER_PHONE_NUMBER", env_path) sender_phone = get_env_variable("NAVER_SENDER_PHONE_NUMBER", env_path)
if not all([access_key, secret_key, service_id, sender_phone]): if not all([access_key, secret_key, service_id, sender_phone]):
print(f"Error: One or more required environment variables are missing in {env_path}.") print(
f"Error: One or more required environment variables are missing in {env_path}."
)
sys.exit(1) sys.exit(1)
timestamp = str(int(time.time() * 1000)) timestamp = str(int(time.time() * 1000))
@@ -52,8 +56,8 @@ def main():
# Create the signature for the API request # Create the signature for the API request
message = f"POST {api_path}\n{timestamp}\n{access_key}" message = f"POST {api_path}\n{timestamp}\n{access_key}"
h = hmac.new(bytes(secret_key, 'UTF-8'), bytes(message, 'UTF-8'), hashlib.sha256) h = hmac.new(bytes(secret_key, "UTF-8"), bytes(message, "UTF-8"), hashlib.sha256)
signature = base64.b64encode(h.digest()).decode('UTF-8') signature = base64.b64encode(h.digest()).decode("UTF-8")
# Construct the JSON request body # Construct the JSON request body
json_body = { json_body = {
@@ -61,19 +65,15 @@ def main():
"contentType": "COMM", "contentType": "COMM",
"countryCode": "82", "countryCode": "82",
"from": sender_phone, "from": sender_phone,
"content": "[Baron 통합로그인] Test message from Python script.", "content": "[Baron 로그인] Test message from Python script.",
"messages": [ "messages": [{"to": recipient_phone}],
{
"to": recipient_phone
}
]
} }
headers = { headers = {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
"x-ncp-apigw-timestamp": timestamp, "x-ncp-apigw-timestamp": timestamp,
"x-ncp-iam-access-key": access_key, "x-ncp-iam-access-key": access_key,
"x-ncp-apigw-signature-v2": signature "x-ncp-apigw-signature-v2": signature,
} }
print("========================================") print("========================================")
@@ -87,12 +87,12 @@ def main():
try: try:
response = requests.post(api_url, headers=headers, json=json_body) response = requests.post(api_url, headers=headers, json=json_body)
response.raise_for_status() # Raise an exception for HTTP errors response.raise_for_status() # Raise an exception for HTTP errors
print("API Response:") print("API Response:")
print(json.dumps(response.json(), indent=2, ensure_ascii=False)) print(json.dumps(response.json(), indent=2, ensure_ascii=False))
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"Request failed: {e}") print(f"Request failed: {e}")
if hasattr(e, 'response') and e.response is not None: if hasattr(e, "response") and e.response is not None:
print("API Error Response:") print("API Error Response:")
try: try:
print(json.dumps(e.response.json(), indent=2, ensure_ascii=False)) print(json.dumps(e.response.json(), indent=2, ensure_ascii=False))
@@ -106,5 +106,6 @@ def main():
print(" Request complete.") print(" Request complete.")
print("========================================") print("========================================")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1028,7 +1028,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
"Baron 통합로그인", "Baron 로그인",
style: TextStyle( style: TextStyle(
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -801,10 +801,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
String _appLabelForPath(String path) { String _appLabelForPath(String path) {
if (path.startsWith('/api/v1/auth')) { if (path.startsWith('/api/v1/auth')) {
return 'Baron 통합로그인'; return 'Baron 로그인';
} }
if (path.startsWith('/api/v1/user')) { if (path.startsWith('/api/v1/user')) {
return 'Baron 통합로그인'; return 'Baron 로그인';
} }
if (path.startsWith('/api/v1/dev')) { if (path.startsWith('/api/v1/dev')) {
return 'Dev Console'; return 'Dev Console';
@@ -812,7 +812,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (path.startsWith('/api/v1/admin')) { if (path.startsWith('/api/v1/admin')) {
return 'Admin Console'; return 'Admin Console';
} }
return 'Baron 통합로그인'; return 'Baron 로그인';
} }
@override @override
@@ -831,7 +831,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
backgroundColor: _subtle, backgroundColor: _subtle,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Baron 통합로그인', 'Baron 로그인',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
elevation: 0, elevation: 0,
@@ -890,7 +890,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildPastRps(isMobile), _buildPastRps(isMobile),
const SizedBox(height: 28), const SizedBox(height: 28),
_buildSectionTitle('접속이력', 'Baron 통합로그인 기준의 최근 접근 기록입니다.'), _buildSectionTitle('접속이력', 'Baron 로그인 기준의 최근 접근 기록입니다.'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAccessHistory(timelineWide), _buildAccessHistory(timelineWide),
], ],

View File

@@ -971,7 +971,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
backgroundColor: _subtle, backgroundColor: _subtle,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Baron 통합로그인', 'Baron 로그인',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
elevation: 0, elevation: 0,

View File

@@ -295,7 +295,7 @@ class BaronSSOApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp.router( return MaterialApp.router(
title: 'Baron 통합로그인', title: 'Baron 로그인',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@@ -14,26 +14,29 @@
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="/"> <base href="/" />
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Baron 통합로그인"> <meta name="apple-mobile-web-app-title" content="Baron 로그인" />
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png" />
<title>Baron 통합로그인</title> <title>Baron 로그인</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link
</head> href="https://fonts.googleapis.com/icon?family=Material+Icons"
<body> rel="stylesheet"
<script src="flutter_bootstrap.js" async></script> />
</body> </head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html> </html>

View File

@@ -1,11 +1,11 @@
{ {
"name": "Baron 통합로그인", "name": "Baron 로그인",
"short_name": "Baron 통합로그인", "short_name": "Baron 로그인",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#0175C2",
"theme_color": "#0175C2", "theme_color": "#0175C2",
"description": "Baron 통합로그인 사용자 포털.", "description": "Baron 로그인 사용자 포털.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
"icons": [ "icons": [