forked from baron/baron-sso
Devfront back-channel logout 설정 UI 및 i18n 추가
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "권한에 대한 설명"
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
Reference in New Issue
Block a user