diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx index 4657e624..d9487d72 100644 --- a/devfront/src/features/clients/ClientDetailsPage.tsx +++ b/devfront/src/features/clients/ClientDetailsPage.tsx @@ -175,6 +175,7 @@ function ClientDetailsPage() { } const client = data?.client; + const isHeadlessLogin = client?.metadata?.headless_login_enabled === true; if (!client) { return null; } @@ -213,16 +214,21 @@ function ClientDetailsPage() { }, ]; - const hasClientSecret = client.type === "private"; + const hasClientSecret = client.type === "private" && !isHeadlessLogin; const secretPlaceholder = "SECRET_NOT_AVAILABLE"; const clientSecret = hasClientSecret ? client?.clientSecret || secretPlaceholder : t("ui.common.na", "N/A"); const displaySecret = !hasClientSecret - ? t( - "msg.dev.clients.details.secret_not_applicable", - "PKCE 앱에는 Client Secret이 없습니다.", - ) + ? isHeadlessLogin + ? t( + "msg.dev.clients.details.secret_not_applicable_headless", + "이 앱은 Headless Login용 signed key 인증을 사용하므로 Client Secret을 사용하지 않습니다.", + ) + : t( + "msg.dev.clients.details.secret_not_applicable", + "PKCE 앱에는 Client Secret이 없습니다.", + ) : clientSecret === secretPlaceholder ? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE") : clientSecret; @@ -394,10 +400,15 @@ function ClientDetailsPage() { {!hasClientSecret ? (

- {t( - "msg.dev.clients.details.secret_not_applicable", - "PKCE 앱에는 Client Secret이 없습니다.", - )} + {isHeadlessLogin + ? t( + "msg.dev.clients.details.secret_not_applicable_headless", + "이 앱은 Headless Login용 signed key 인증을 사용하므로 Client Secret을 사용하지 않습니다.", + ) + : t( + "msg.dev.clients.details.secret_not_applicable", + "PKCE 앱에는 Client Secret이 없습니다.", + )}

) : null} diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index c0d76c06..f7ff4a06 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -391,12 +391,13 @@ function ClientGeneralPage() { useEffect(() => { if (!data) return; const { client } = data; + const metadata = client.metadata ?? {}; + const headlessEnabled = !!metadata.headless_login_enabled; + setName(client.name || client.id); - setClientType(client.type); + setClientType(headlessEnabled ? "private" : client.type); setStatus(client.status); setInitialStatus(client.status); - - const metadata = client.metadata ?? {}; if (typeof metadata.description === "string") setDescription(metadata.description); if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url); @@ -412,7 +413,6 @@ function ClientGeneralPage() { if (typeof metadata.auto_login_url === "string") setAutoLoginUrl(metadata.auto_login_url); - const headlessEnabled = !!metadata.headless_login_enabled; setHeadlessLoginEnabled(headlessEnabled); const restrictedTenants = Array.isArray(metadata.allowed_tenants) ? metadata.allowed_tenants @@ -532,18 +532,25 @@ function ClientGeneralPage() { const handleSecurityProfileChange = (profile: SecurityProfile) => { setClientType(profile); if (profile === "pkce") { - setTokenEndpointAuthMethod( - headlessLoginEnabled ? "private_key_jwt" : "none", - ); + setHeadlessLoginEnabled(false); + setTokenEndpointAuthMethod("none"); } else { - setTokenEndpointAuthMethod("client_secret_basic"); + setTokenEndpointAuthMethod( + headlessLoginEnabled ? "private_key_jwt" : "client_secret_basic", + ); } }; const handleHeadlessToggle = (enabled: boolean) => { setHeadlessLoginEnabled(enabled); - if (clientType === "pkce") { - setTokenEndpointAuthMethod(enabled ? "private_key_jwt" : "none"); + if (enabled) { + setClientType("private"); + setTokenEndpointAuthMethod("private_key_jwt"); + return; + } + + if (clientType === "private") { + setTokenEndpointAuthMethod("client_secret_basic"); } }; @@ -974,14 +981,14 @@ function ClientGeneralPage() { .map((scope) => scope.name.trim()) .filter(Boolean); - const effectiveTokenEndpointAuthMethod = - clientType === "pkce" && headlessLoginEnabled - ? "none" - : tokenEndpointAuthMethod; + const persistedClientType = headlessLoginEnabled ? "pkce" : clientType; + const effectiveTokenEndpointAuthMethod = headlessLoginEnabled + ? "none" + : tokenEndpointAuthMethod; const payload: ClientUpsertRequest = { name, - type: clientType, + type: persistedClientType, scopes: scopeNames, tokenEndpointAuthMethod: effectiveTokenEndpointAuthMethod, jwksUri: @@ -1003,14 +1010,10 @@ function ClientGeneralPage() { id_token_claims: normalizedIdTokenClaims, token_endpoint_auth_method: effectiveTokenEndpointAuthMethod, headless_login_enabled: headlessLoginEnabled, - headless_token_endpoint_auth_method: - clientType === "pkce" && headlessLoginEnabled - ? tokenEndpointAuthMethod - : undefined, - headless_jwks_uri: - clientType === "pkce" && headlessLoginEnabled - ? trimmedJwksUri - : undefined, + headless_token_endpoint_auth_method: headlessLoginEnabled + ? tokenEndpointAuthMethod + : undefined, + headless_jwks_uri: headlessLoginEnabled ? trimmedJwksUri : undefined, tenant_access_restricted: tenantAccessRestricted, allowed_tenants: tenantAccessRestricted ? normalizedAllowedTenantIds @@ -2291,6 +2294,38 @@ function ClientGeneralPage() { {securityProfile === "private" ? "✓" : ""} + + {securityProfile === "private" && ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > +
+ +

+ {t( + "msg.dev.clients.general.security.headless_login_enable_help", + "Baron SSO 로그인 창 대신 RP 자체 로그인 UI를 사용하고, RP backend의 서명 키로 클라이언트를 검증하려는 경우 활성화합니다.", + )} +

+
+ +
+ )} {/* 4. Public Key Registration (Headless Login) */} - {clientType === "pkce" && headlessLoginEnabled && ( + {headlessLoginEnabled && (
@@ -2373,7 +2376,7 @@ function ClientGeneralPage() { {t( "msg.dev.clients.general.public_key.subtitle", - "Headless Login 판정에 필요한 공개키와 관련 설정을 관리합니다.", + "Server side App의 Headless Login capability에 필요한 공개키와 검증 정보를 관리합니다.", )}
@@ -2392,7 +2395,7 @@ function ClientGeneralPage() {

{t( "msg.dev.clients.general.public_key.headless_help", - "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.", + "RP가 자체 로그인 UI를 제공하더라도 실제 인증 흐름은 Baron API와 RP backend의 signed key 검증을 통해 이어집니다.", )}

diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 5dc148c0..2d1fd352 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -524,18 +524,28 @@ function ClientsPage() { - - {client.type === "private" - ? t("ui.dev.clients.type.private", "Server side App") - : client.metadata?.headless_login_enabled +
+ + {client.metadata?.headless_login_enabled ? t( - "ui.dev.clients.type.pkce_headless", - "PKCE (Headless Login)", + "ui.dev.clients.type.private_headless", + "Server side App (Headless Login)", ) - : t("ui.dev.clients.type.pkce", "PKCE")} - + : client.type === "private" + ? t( + "ui.dev.clients.type.private", + "Server side App", + ) + : t("ui.dev.clients.type.pkce", "PKCE")} + +