1
0
forked from baron/baron-sso

headless login SSA UX 재구성

This commit is contained in:
2026-05-04 14:11:07 +09:00
parent 0978adcee5
commit b888b33cde
6 changed files with 109 additions and 79 deletions

View File

@@ -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() {
</div>
{!hasClientSecret ? (
<p className="mt-2 text-sm text-muted-foreground">
{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이 없습니다.",
)}
</p>
) : null}
</div>

View File

@@ -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() {
<span className="absolute right-4 top-4 text-primary">
{securityProfile === "private" ? "✓" : ""}
</span>
{securityProfile === "private" && (
<div
className="mt-4 flex items-center justify-between border-t border-primary/20 pt-4"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div className="space-y-0.5">
<Label
className="cursor-pointer text-xs font-bold"
htmlFor="headless-login-toggle"
>
{t(
"ui.dev.clients.general.security.headless_login_enable",
"Headless Login (자체 로그인 UI 사용)",
)}
</Label>
<p className="text-[10px] text-muted-foreground">
{t(
"msg.dev.clients.general.security.headless_login_enable_help",
"Baron SSO 로그인 창 대신 RP 자체 로그인 UI를 사용하고, RP backend의 서명 키로 클라이언트를 검증하려는 경우 활성화합니다.",
)}
</p>
</div>
<Switch
id="headless-login-toggle"
checked={headlessLoginEnabled}
onCheckedChange={handleHeadlessToggle}
disabled={isGeneralSettingsReadOnly}
/>
</div>
)}
</label>
<label
@@ -2321,45 +2356,13 @@ function ClientGeneralPage() {
<span className="absolute right-4 top-4 text-primary">
{securityProfile === "pkce" ? "✓" : ""}
</span>
{securityProfile === "pkce" && (
<div
className="mt-4 pt-4 border-t border-primary/20 flex items-center justify-between"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div className="space-y-0.5">
<Label
className="text-xs font-bold cursor-pointer"
htmlFor="headless-login-toggle"
>
{t(
"ui.dev.clients.general.security.headless_login_enable",
"Headless Login (자체 로그인 UI 사용)",
)}
</Label>
<p className="text-[10px] text-muted-foreground">
{t(
"ui.dev.clients.general.security.headless_login_enable_help",
"Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.",
)}
</p>
</div>
<Switch
id="headless-login-toggle"
checked={headlessLoginEnabled}
onCheckedChange={handleHeadlessToggle}
disabled={isGeneralSettingsReadOnly}
/>
</div>
)}
</label>
</div>
</CardContent>
</Card>
{/* 4. Public Key Registration (Headless Login) */}
{clientType === "pkce" && headlessLoginEnabled && (
{headlessLoginEnabled && (
<Card className="glass-panel border-primary/20">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-start justify-between gap-3">
@@ -2373,7 +2376,7 @@ function ClientGeneralPage() {
<CardDescription>
{t(
"msg.dev.clients.general.public_key.subtitle",
"Headless Login 판정에 필요한 공개키와 관련 설정을 관리합니다.",
"Server side App의 Headless Login capability에 필요한 공개키와 검증 정보를 관리합니다.",
)}
</CardDescription>
</div>
@@ -2392,7 +2395,7 @@ function ClientGeneralPage() {
<p className="mt-1 text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.public_key.headless_help",
"애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.",
"RP가 자체 로그인 UI를 제공하더라도 실제 인증 흐름은 Baron API와 RP backend의 signed key 검증을 통해 이어집니다.",
)}
</p>
</div>

View File

@@ -524,18 +524,28 @@ function ClientsPage() {
</div>
</TableCell>
<TableCell>
<Badge
variant={client.type === "private" ? "success" : "muted"}
>
{client.type === "private"
? t("ui.dev.clients.type.private", "Server side App")
: client.metadata?.headless_login_enabled
<div className="flex flex-wrap items-center gap-2">
<Badge
variant={
client.type === "private" ||
client.metadata?.headless_login_enabled
? "success"
: "muted"
}
>
{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")}
</Badge>
: client.type === "private"
? t(
"ui.dev.clients.type.private",
"Server side App",
)
: t("ui.dev.clients.type.pkce", "PKCE")}
</Badge>
</div>
</TableCell>
<TableCell>
<Badge