1
0
forked from baron/baron-sso

Devfront back-channel logout 설정 UI 및 i18n 추가

This commit is contained in:
2026-05-04 11:00:02 +09:00
parent 068d0adbd4
commit 0664640c6f
5 changed files with 214 additions and 0 deletions

View File

@@ -271,6 +271,29 @@ function isValidUrl(value: string): boolean {
}
}
function isValidBackchannelLogoutUrl(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) {
return true;
}
try {
const url = new URL(trimmed);
if (url.hash) {
return false;
}
if (url.protocol === "https:") {
return true;
}
if (url.protocol !== "http:") {
return false;
}
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
} catch {
return false;
}
}
function formatDateTime(value?: string) {
if (!value) return "-";
const date = new Date(value);
@@ -315,6 +338,11 @@ function ClientGeneralPage() {
const [status, setStatus] = useState<ClientStatus>("active");
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
const [redirectUris, setRedirectUris] = useState("");
const [backchannelLogoutUri, setBackchannelLogoutUri] = useState("");
const [backchannelLogoutSessionRequired, setBackchannelLogoutSessionRequired] =
useState(false);
const [isBackchannelSessionRequiredInfoOpen, setIsBackchannelSessionRequiredInfoOpen] =
useState(false);
const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
const [tenantSearch, setTenantSearch] = useState("");
@@ -368,6 +396,14 @@ function ClientGeneralPage() {
if (typeof metadata.description === "string")
setDescription(metadata.description);
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
setBackchannelLogoutUri(
client.backchannelLogoutUri ||
readMetadataString(metadata, "backchannel_logout_uri"),
);
setBackchannelLogoutSessionRequired(
client.backchannelLogoutSessionRequired === true ||
metadata.backchannel_logout_session_required === true,
);
setAutoLoginSupported(metadata.auto_login_supported === true);
if (typeof metadata.auto_login_url === "string")
setAutoLoginUrl(metadata.auto_login_url);
@@ -468,6 +504,11 @@ function ClientGeneralPage() {
const trimmedAutoLoginUrl = autoLoginUrl.trim();
const hasLogoUrl = trimmedLogoUrl.length > 0;
const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
const trimmedBackchannelLogoutUri = backchannelLogoutUri.trim();
const hasBackchannelLogoutUri = trimmedBackchannelLogoutUri.length > 0;
const hasValidBackchannelLogoutUri =
!hasBackchannelLogoutUri ||
isValidBackchannelLogoutUrl(trimmedBackchannelLogoutUri);
const hasValidAutoLoginUrl =
!autoLoginSupported ||
(trimmedAutoLoginUrl.length > 0 && isValidUrl(trimmedAutoLoginUrl));
@@ -893,6 +934,14 @@ function ClientGeneralPage() {
),
);
}
if (hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri) {
throw new Error(
t(
"msg.dev.clients.general.backchannel_logout.invalid",
"Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.",
),
);
}
if (isGeneralSettingsReadOnly) {
throw new Error(
t(
@@ -936,6 +985,11 @@ function ClientGeneralPage() {
trimmedJwksUri
? trimmedJwksUri
: undefined,
backchannelLogoutUri: trimmedBackchannelLogoutUri || undefined,
backchannelLogoutSessionRequired:
trimmedBackchannelLogoutUri !== ""
? backchannelLogoutSessionRequired
: false,
metadata: {
description,
logo_url: trimmedLogoUrl,
@@ -957,6 +1011,11 @@ function ClientGeneralPage() {
allowed_tenants: tenantAccessRestricted
? normalizedAllowedTenantIds
: [],
backchannel_logout_uri: trimmedBackchannelLogoutUri || undefined,
backchannel_logout_session_required:
trimmedBackchannelLogoutUri !== ""
? backchannelLogoutSessionRequired
: undefined,
},
};
@@ -1493,6 +1552,118 @@ function ClientGeneralPage() {
</div>
)}
<div className="space-y-4 border-b border-border pb-6 mb-6">
<div className="space-y-2">
<Label
className="text-sm font-semibold"
htmlFor="backchannel-logout-uri"
>
{t(
"ui.dev.clients.general.backchannel_logout.uri",
"Back-Channel Logout URI",
)}
</Label>
<Input
id="backchannel-logout-uri"
value={backchannelLogoutUri}
onChange={(e) => setBackchannelLogoutUri(e.target.value)}
placeholder={t(
"ui.dev.clients.general.backchannel_logout.uri_placeholder",
"https://rp.example.com/oidc/backchannel-logout",
)}
className="font-mono text-sm"
disabled={isGeneralSettingsReadOnly}
/>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.backchannel_logout.uri_help",
"Baron이 세션 종료 이벤트를 서버 간 POST로 전달할 RP endpoint입니다.",
)}
</p>
{hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri ? (
<p className="text-xs text-destructive">
{t(
"msg.dev.clients.general.backchannel_logout.invalid",
"Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.",
)}
</p>
) : null}
</div>
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/20 px-4 py-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Label
className="text-sm font-semibold"
htmlFor="backchannel-logout-session-required"
>
{t(
"ui.dev.clients.general.backchannel_logout.session_required",
"SID Claim Required",
)}
</Label>
<button
type="button"
className={`rounded-full p-0.5 transition-colors ${
isBackchannelSessionRequiredInfoOpen
? "text-primary"
: "text-muted-foreground/60 hover:text-primary"
}`}
onClick={() =>
setIsBackchannelSessionRequiredInfoOpen(
(prev) => !prev,
)
}
aria-label={t(
"ui.dev.clients.general.backchannel_logout.session_required_info",
"SID Claim Required 설명 보기",
)}
>
{isBackchannelSessionRequiredInfoOpen ? (
<X className="h-3.5 w-3.5" />
) : (
<Info className="h-3.5 w-3.5" />
)}
</button>
</div>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.backchannel_logout.session_required_help",
"RP가 logout_token에 sid claim이 포함된 경우에만 처리하도록 요구할 때 사용합니다.",
)}
</p>
{isBackchannelSessionRequiredInfoOpen ? (
<div className="mt-2 animate-in fade-in slide-in-from-top-1 rounded-lg border border-primary/20 bg-primary/5 p-3 text-xs leading-relaxed text-foreground shadow-sm">
<div className="flex items-center gap-1.5 font-bold text-primary mb-1">
<Info className="h-3 w-3" />
{t("ui.common.info", "상세 안내")}
</div>
<div>
{t(
"msg.dev.clients.general.backchannel_logout.session_required_on",
"켜면: logout_token 안에 sid가 있을 때만 로그아웃 처리",
)}
</div>
<div>
{t(
"msg.dev.clients.general.backchannel_logout.session_required_off",
"끄면: sid가 없어도 sub만으로 로그아웃 처리 가능",
)}
</div>
</div>
) : null}
</div>
<Switch
id="backchannel-logout-session-required"
checked={backchannelLogoutSessionRequired}
onCheckedChange={setBackchannelLogoutSessionRequired}
disabled={
isGeneralSettingsReadOnly || !hasBackchannelLogoutUri
}
/>
</div>
</div>
<div className="rounded-md border border-border overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted/50 border-b border-border text-xs uppercase tracking-wider text-muted-foreground">

View File

@@ -12,6 +12,8 @@ export type ClientSummary = {
clientSecret?: string;
tokenEndpointAuthMethod?: string;
jwksUri?: string;
backchannelLogoutUri?: string;
backchannelLogoutSessionRequired?: boolean;
redirectUris: string[];
scopes: string[];
metadata?: Record<string, unknown>;
@@ -118,6 +120,8 @@ export type ClientUpsertRequest = {
responseTypes?: string[];
tokenEndpointAuthMethod?: string;
jwksUri?: string;
backchannelLogoutUri?: string;
backchannelLogoutSessionRequired?: boolean;
metadata?: Record<string, unknown>;
};

View File

@@ -432,6 +432,13 @@ subtitle = "Set the application name, description, and logo."
[msg.dev.clients.general.redirect]
help = "Enter the redirect URIs. You can modify them in the Federation tab after creation."
[msg.dev.clients.general.backchannel_logout]
uri_help = "RP endpoint that receives Baron's session termination event via server-to-server POST."
invalid = "The Back-Channel Logout URI format is invalid. Production requires https, and local development only allows http on localhost/127.0.0.1."
session_required_help = "Use this when the RP should process logout_token only if the sid claim is included."
session_required_on = "On: process logout only when the logout_token contains a sid."
session_required_off = "Off: process logout using sub even if sid is missing."
[msg.dev.clients.general.scopes]
empty = "No scopes registered."
subtitle = "Define the permission scopes this application can request."
@@ -1484,6 +1491,12 @@ title = "Application Identity"
label = "Redirect URIs"
placeholder = "Placeholder"
[ui.dev.clients.general.backchannel_logout]
uri = "Back-Channel Logout URI"
uri_placeholder = "https://rp.example.com/oidc/backchannel-logout"
session_required = "SID Claim Required"
session_required_info = "Show SID Claim Required help"
[ui.dev.clients.general.scopes]
add = "Scope Add"
description_placeholder = "Description Placeholder"

View File

@@ -432,6 +432,13 @@ subtitle = "앱 이름과 설명, 로고를 설정합니다."
[msg.dev.clients.general.redirect]
help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다."
[msg.dev.clients.general.backchannel_logout]
uri_help = "Baron이 세션 종료 이벤트를 서버 간 POST로 전달할 RP endpoint입니다."
invalid = "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다."
session_required_help = "RP가 logout_token에 sid claim이 포함된 경우에만 처리하도록 요구할 때 사용합니다."
session_required_on = "켜면: logout_token 안에 sid가 있을 때만 로그아웃 처리"
session_required_off = "끄면: sid가 없어도 sub만으로 로그아웃 처리 가능"
[msg.dev.clients.general.scopes]
empty = "등록된 스코프가 없습니다."
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
@@ -1483,6 +1490,12 @@ title = "애플리케이션 정보"
label = "리디렉션 URI"
placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)"
[ui.dev.clients.general.backchannel_logout]
uri = "Back-Channel Logout URI"
uri_placeholder = "https://rp.example.com/oidc/backchannel-logout"
session_required = "SID Claim Required"
session_required_info = "SID Claim Required 설명 보기"
[ui.dev.clients.general.scopes]
add = "스코프 추가"
description_placeholder = "권한에 대한 설명"

View File

@@ -479,6 +479,13 @@ subtitle = ""
[msg.dev.clients.general.redirect]
help = ""
[msg.dev.clients.general.backchannel_logout]
uri_help = ""
invalid = ""
session_required_help = ""
session_required_on = ""
session_required_off = ""
[msg.dev.clients.general.scopes]
empty = ""
subtitle = ""
@@ -1539,6 +1546,12 @@ title = ""
label = ""
placeholder = ""
[ui.dev.clients.general.backchannel_logout]
uri = ""
uri_placeholder = ""
session_required = ""
session_required_info = ""
[ui.dev.clients.general.scopes]
add = ""
description_placeholder = ""