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) {
|
function formatDateTime(value?: string) {
|
||||||
if (!value) return "-";
|
if (!value) return "-";
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
@@ -315,6 +338,11 @@ function ClientGeneralPage() {
|
|||||||
const [status, setStatus] = useState<ClientStatus>("active");
|
const [status, setStatus] = useState<ClientStatus>("active");
|
||||||
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
||||||
const [redirectUris, setRedirectUris] = useState("");
|
const [redirectUris, setRedirectUris] = useState("");
|
||||||
|
const [backchannelLogoutUri, setBackchannelLogoutUri] = useState("");
|
||||||
|
const [backchannelLogoutSessionRequired, setBackchannelLogoutSessionRequired] =
|
||||||
|
useState(false);
|
||||||
|
const [isBackchannelSessionRequiredInfoOpen, setIsBackchannelSessionRequiredInfoOpen] =
|
||||||
|
useState(false);
|
||||||
const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
|
const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
|
||||||
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
|
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
|
||||||
const [tenantSearch, setTenantSearch] = useState("");
|
const [tenantSearch, setTenantSearch] = useState("");
|
||||||
@@ -368,6 +396,14 @@ function ClientGeneralPage() {
|
|||||||
if (typeof metadata.description === "string")
|
if (typeof metadata.description === "string")
|
||||||
setDescription(metadata.description);
|
setDescription(metadata.description);
|
||||||
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
|
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);
|
setAutoLoginSupported(metadata.auto_login_supported === true);
|
||||||
if (typeof metadata.auto_login_url === "string")
|
if (typeof metadata.auto_login_url === "string")
|
||||||
setAutoLoginUrl(metadata.auto_login_url);
|
setAutoLoginUrl(metadata.auto_login_url);
|
||||||
@@ -468,6 +504,11 @@ function ClientGeneralPage() {
|
|||||||
const trimmedAutoLoginUrl = autoLoginUrl.trim();
|
const trimmedAutoLoginUrl = autoLoginUrl.trim();
|
||||||
const hasLogoUrl = trimmedLogoUrl.length > 0;
|
const hasLogoUrl = trimmedLogoUrl.length > 0;
|
||||||
const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
|
const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
|
||||||
|
const trimmedBackchannelLogoutUri = backchannelLogoutUri.trim();
|
||||||
|
const hasBackchannelLogoutUri = trimmedBackchannelLogoutUri.length > 0;
|
||||||
|
const hasValidBackchannelLogoutUri =
|
||||||
|
!hasBackchannelLogoutUri ||
|
||||||
|
isValidBackchannelLogoutUrl(trimmedBackchannelLogoutUri);
|
||||||
const hasValidAutoLoginUrl =
|
const hasValidAutoLoginUrl =
|
||||||
!autoLoginSupported ||
|
!autoLoginSupported ||
|
||||||
(trimmedAutoLoginUrl.length > 0 && isValidUrl(trimmedAutoLoginUrl));
|
(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) {
|
if (isGeneralSettingsReadOnly) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
t(
|
t(
|
||||||
@@ -936,6 +985,11 @@ function ClientGeneralPage() {
|
|||||||
trimmedJwksUri
|
trimmedJwksUri
|
||||||
? trimmedJwksUri
|
? trimmedJwksUri
|
||||||
: undefined,
|
: undefined,
|
||||||
|
backchannelLogoutUri: trimmedBackchannelLogoutUri || undefined,
|
||||||
|
backchannelLogoutSessionRequired:
|
||||||
|
trimmedBackchannelLogoutUri !== ""
|
||||||
|
? backchannelLogoutSessionRequired
|
||||||
|
: false,
|
||||||
metadata: {
|
metadata: {
|
||||||
description,
|
description,
|
||||||
logo_url: trimmedLogoUrl,
|
logo_url: trimmedLogoUrl,
|
||||||
@@ -957,6 +1011,11 @@ function ClientGeneralPage() {
|
|||||||
allowed_tenants: tenantAccessRestricted
|
allowed_tenants: tenantAccessRestricted
|
||||||
? normalizedAllowedTenantIds
|
? normalizedAllowedTenantIds
|
||||||
: [],
|
: [],
|
||||||
|
backchannel_logout_uri: trimmedBackchannelLogoutUri || undefined,
|
||||||
|
backchannel_logout_session_required:
|
||||||
|
trimmedBackchannelLogoutUri !== ""
|
||||||
|
? backchannelLogoutSessionRequired
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1493,6 +1552,118 @@ function ClientGeneralPage() {
|
|||||||
</div>
|
</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">
|
<div className="rounded-md border border-border overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-muted/50 border-b border-border text-xs uppercase tracking-wider text-muted-foreground">
|
<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;
|
clientSecret?: string;
|
||||||
tokenEndpointAuthMethod?: string;
|
tokenEndpointAuthMethod?: string;
|
||||||
jwksUri?: string;
|
jwksUri?: string;
|
||||||
|
backchannelLogoutUri?: string;
|
||||||
|
backchannelLogoutSessionRequired?: boolean;
|
||||||
redirectUris: string[];
|
redirectUris: string[];
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
@@ -118,6 +120,8 @@ export type ClientUpsertRequest = {
|
|||||||
responseTypes?: string[];
|
responseTypes?: string[];
|
||||||
tokenEndpointAuthMethod?: string;
|
tokenEndpointAuthMethod?: string;
|
||||||
jwksUri?: string;
|
jwksUri?: string;
|
||||||
|
backchannelLogoutUri?: string;
|
||||||
|
backchannelLogoutSessionRequired?: boolean;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -432,6 +432,13 @@ subtitle = "Set the application name, description, and logo."
|
|||||||
[msg.dev.clients.general.redirect]
|
[msg.dev.clients.general.redirect]
|
||||||
help = "Enter the redirect URIs. You can modify them in the Federation tab after creation."
|
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]
|
[msg.dev.clients.general.scopes]
|
||||||
empty = "No scopes registered."
|
empty = "No scopes registered."
|
||||||
subtitle = "Define the permission scopes this application can request."
|
subtitle = "Define the permission scopes this application can request."
|
||||||
@@ -1484,6 +1491,12 @@ title = "Application Identity"
|
|||||||
label = "Redirect URIs"
|
label = "Redirect URIs"
|
||||||
placeholder = "Placeholder"
|
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]
|
[ui.dev.clients.general.scopes]
|
||||||
add = "Scope Add"
|
add = "Scope Add"
|
||||||
description_placeholder = "Description Placeholder"
|
description_placeholder = "Description Placeholder"
|
||||||
|
|||||||
@@ -432,6 +432,13 @@ subtitle = "앱 이름과 설명, 로고를 설정합니다."
|
|||||||
[msg.dev.clients.general.redirect]
|
[msg.dev.clients.general.redirect]
|
||||||
help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다."
|
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]
|
[msg.dev.clients.general.scopes]
|
||||||
empty = "등록된 스코프가 없습니다."
|
empty = "등록된 스코프가 없습니다."
|
||||||
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
||||||
@@ -1483,6 +1490,12 @@ title = "애플리케이션 정보"
|
|||||||
label = "리디렉션 URI"
|
label = "리디렉션 URI"
|
||||||
placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)"
|
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]
|
[ui.dev.clients.general.scopes]
|
||||||
add = "스코프 추가"
|
add = "스코프 추가"
|
||||||
description_placeholder = "권한에 대한 설명"
|
description_placeholder = "권한에 대한 설명"
|
||||||
|
|||||||
@@ -479,6 +479,13 @@ subtitle = ""
|
|||||||
[msg.dev.clients.general.redirect]
|
[msg.dev.clients.general.redirect]
|
||||||
help = ""
|
help = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.general.backchannel_logout]
|
||||||
|
uri_help = ""
|
||||||
|
invalid = ""
|
||||||
|
session_required_help = ""
|
||||||
|
session_required_on = ""
|
||||||
|
session_required_off = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.scopes]
|
[msg.dev.clients.general.scopes]
|
||||||
empty = ""
|
empty = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
@@ -1539,6 +1546,12 @@ title = ""
|
|||||||
label = ""
|
label = ""
|
||||||
placeholder = ""
|
placeholder = ""
|
||||||
|
|
||||||
|
[ui.dev.clients.general.backchannel_logout]
|
||||||
|
uri = ""
|
||||||
|
uri_placeholder = ""
|
||||||
|
session_required = ""
|
||||||
|
session_required_info = ""
|
||||||
|
|
||||||
[ui.dev.clients.general.scopes]
|
[ui.dev.clients.general.scopes]
|
||||||
add = ""
|
add = ""
|
||||||
description_placeholder = ""
|
description_placeholder = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user