From 0664640c6f1893408a101ad836a7ea6d8d04fe24 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 4 May 2026 11:00:02 +0900 Subject: [PATCH] =?UTF-8?q?Devfront=20back-channel=20logout=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20UI=20=EB=B0=8F=20i18n=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientGeneralPage.tsx | 171 ++++++++++++++++++ devfront/src/lib/devApi.ts | 4 + devfront/src/locales/en.toml | 13 ++ devfront/src/locales/ko.toml | 13 ++ devfront/src/locales/template.toml | 13 ++ 5 files changed, 214 insertions(+) diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 79352aa2..a6dcd925 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -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("active"); const [initialStatus, setInitialStatus] = useState("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([]); 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() { )} +
+
+ + 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} + /> +

+ {t( + "msg.dev.clients.general.backchannel_logout.uri_help", + "Baron이 세션 종료 이벤트를 서버 간 POST로 전달할 RP endpoint입니다.", + )} +

+ {hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri ? ( +

+ {t( + "msg.dev.clients.general.backchannel_logout.invalid", + "Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.", + )} +

+ ) : null} +
+ +
+
+
+ + +
+

+ {t( + "msg.dev.clients.general.backchannel_logout.session_required_help", + "RP가 logout_token에 sid claim이 포함된 경우에만 처리하도록 요구할 때 사용합니다.", + )} +

+ {isBackchannelSessionRequiredInfoOpen ? ( +
+
+ + {t("ui.common.info", "상세 안내")} +
+
+ {t( + "msg.dev.clients.general.backchannel_logout.session_required_on", + "켜면: logout_token 안에 sid가 있을 때만 로그아웃 처리", + )} +
+
+ {t( + "msg.dev.clients.general.backchannel_logout.session_required_off", + "끄면: sid가 없어도 sub만으로 로그아웃 처리 가능", + )} +
+
+ ) : null} +
+ +
+
+
diff --git a/devfront/src/lib/devApi.ts b/devfront/src/lib/devApi.ts index 0a6574f2..f1422f59 100644 --- a/devfront/src/lib/devApi.ts +++ b/devfront/src/lib/devApi.ts @@ -12,6 +12,8 @@ export type ClientSummary = { clientSecret?: string; tokenEndpointAuthMethod?: string; jwksUri?: string; + backchannelLogoutUri?: string; + backchannelLogoutSessionRequired?: boolean; redirectUris: string[]; scopes: string[]; metadata?: Record; @@ -118,6 +120,8 @@ export type ClientUpsertRequest = { responseTypes?: string[]; tokenEndpointAuthMethod?: string; jwksUri?: string; + backchannelLogoutUri?: string; + backchannelLogoutSessionRequired?: boolean; metadata?: Record; }; diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index a6491f7c..3733475c 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -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" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 1cec9c82..912012ca 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -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 = "권한에 대한 설명" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 797e5913..f53a4586 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -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 = ""