1
0
forked from baron/baron-sso

fix(headless-login): show full parsed jwks key values

- return the full RSA n value in parsedKeys responses
- render parsed key fields with labels and multiline key material in DevFront
- lock the behavior with backend and Playwright regression tests
This commit is contained in:
Lectom C Han
2026-04-01 18:51:39 +09:00
parent e2379658c2
commit 51f09bf53c
7 changed files with 58 additions and 51 deletions

View File

@@ -3,11 +3,11 @@ package domain
import "time" import "time"
type HeadlessJWKSParsedKey struct { type HeadlessJWKSParsedKey struct {
Kid string `json:"kid,omitempty"` Kid string `json:"kid,omitempty"`
Kty string `json:"kty,omitempty"` Kty string `json:"kty,omitempty"`
Use string `json:"use,omitempty"` Use string `json:"use,omitempty"`
Alg string `json:"alg,omitempty"` Alg string `json:"alg,omitempty"`
NPreview string `json:"nPreview,omitempty"` N string `json:"n,omitempty"`
} }
// HeadlessJWKSCacheState는 headless login용 JWKS 캐시 상태와 최근 동기화 결과를 나타냅니다. // HeadlessJWKSCacheState는 headless login용 JWKS 캐시 상태와 최근 동기화 결과를 나타냅니다.

View File

@@ -10,7 +10,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
@@ -108,14 +107,6 @@ func devTestJWKSFirstKeyString(t *testing.T, jwks map[string]any, field string)
return value return value
} }
func devTestPreviewValue(value string) string {
value = strings.TrimSpace(value)
if len(value) <= 24 {
return value
}
return value[:12] + "..." + value[len(value)-12:]
}
// --- Tests --- // --- Tests ---
func TestListClients_Success(t *testing.T) { func TestListClients_Success(t *testing.T) {
@@ -855,7 +846,7 @@ func TestRefreshHeadlessJWKSCache_ReturnsUpdatedCacheState(t *testing.T) {
privateKey, jwks := mustHeadlessRSAJWK(t) privateKey, jwks := mustHeadlessRSAJWK(t)
_ = privateKey _ = privateKey
jwksBody, _ := json.Marshal(jwks) jwksBody, _ := json.Marshal(jwks)
expectedNPreview := devTestPreviewValue(devTestJWKSFirstKeyString(t, jwks, "n")) expectedN := devTestJWKSFirstKeyString(t, jwks, "n")
redisRepo := &devMockRedisRepo{data: map[string]string{}} redisRepo := &devMockRedisRepo{data: map[string]string{}}
h := &DevHandler{ h := &DevHandler{
Hydra: &service.HydraAdminService{ Hydra: &service.HydraAdminService{
@@ -908,7 +899,7 @@ func TestRefreshHeadlessJWKSCache_ReturnsUpdatedCacheState(t *testing.T) {
assert.Equal(t, "RSA", got.HeadlessJWKSCache.ParsedKeys[0].Kty) assert.Equal(t, "RSA", got.HeadlessJWKSCache.ParsedKeys[0].Kty)
assert.Equal(t, "sig", got.HeadlessJWKSCache.ParsedKeys[0].Use) assert.Equal(t, "sig", got.HeadlessJWKSCache.ParsedKeys[0].Use)
assert.Equal(t, "RS256", got.HeadlessJWKSCache.ParsedKeys[0].Alg) assert.Equal(t, "RS256", got.HeadlessJWKSCache.ParsedKeys[0].Alg)
assert.Equal(t, expectedNPreview, got.HeadlessJWKSCache.ParsedKeys[0].NPreview) assert.Equal(t, expectedN, got.HeadlessJWKSCache.ParsedKeys[0].N)
} }
} }
} }

View File

@@ -390,24 +390,16 @@ func summarizeHeadlessJWKS(raw string) []domain.HeadlessJWKSParsedKey {
parsedKeys := make([]domain.HeadlessJWKSParsedKey, 0, len(document.Keys)) parsedKeys := make([]domain.HeadlessJWKSParsedKey, 0, len(document.Keys))
for _, key := range document.Keys { for _, key := range document.Keys {
parsedKeys = append(parsedKeys, domain.HeadlessJWKSParsedKey{ parsedKeys = append(parsedKeys, domain.HeadlessJWKSParsedKey{
Kid: strings.TrimSpace(key.Kid), Kid: strings.TrimSpace(key.Kid),
Kty: strings.TrimSpace(key.Kty), Kty: strings.TrimSpace(key.Kty),
Use: strings.TrimSpace(key.Use), Use: strings.TrimSpace(key.Use),
Alg: strings.TrimSpace(key.Alg), Alg: strings.TrimSpace(key.Alg),
NPreview: previewHeadlessJWKValue(key.N), N: strings.TrimSpace(key.N),
}) })
} }
return parsedKeys return parsedKeys
} }
func previewHeadlessJWKValue(value string) string {
value = strings.TrimSpace(value)
if len(value) <= 24 {
return value
}
return value[:12] + "..." + value[len(value)-12:]
}
func extractHeadlessKids(keySet *jose.JSONWebKeySet) []string { func extractHeadlessKids(keySet *jose.JSONWebKeySet) []string {
if keySet == nil { if keySet == nil {
return nil return nil

View File

@@ -1313,35 +1313,55 @@ function ClientGeneralPage() {
</p> </p>
</div> </div>
{currentHeadlessJwksCache.parsedKeys?.length ? ( {currentHeadlessJwksCache.parsedKeys?.length ? (
<div className="grid gap-3 lg:grid-cols-2"> <div className="space-y-3">
{currentHeadlessJwksCache.parsedKeys.map((key, index) => ( {currentHeadlessJwksCache.parsedKeys.map((key, index) => (
<div <div
key={`${key.kid || "key"}-${index}`} key={`${key.kid || "key"}-${index}`}
className="rounded-xl border border-border bg-muted/30 p-3" className="rounded-xl border border-border bg-muted/30 p-3"
> >
<div className="flex flex-wrap items-center gap-2"> <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<Badge variant="secondary" className="font-mono"> <div className="space-y-1">
{key.kid || "-"} <p className="text-[11px] font-semibold uppercase text-muted-foreground">
</Badge> KID
<Badge variant="outline" className="font-mono"> </p>
{key.kty || "-"} <p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
</Badge> {key.kid || "-"}
<Badge variant="outline" className="font-mono"> </p>
{key.use || "-"} </div>
</Badge> <div className="space-y-1">
<Badge variant="outline" className="font-mono"> <p className="text-[11px] font-semibold uppercase text-muted-foreground">
{key.alg || "-"} KTY
</Badge> </p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.kty || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
USE
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.use || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
ALG
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.alg || "-"}
</p>
</div>
</div> </div>
<div className="mt-3 space-y-1"> <div className="mt-3 space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground"> <p className="text-[11px] font-semibold uppercase text-muted-foreground">
{t( {t(
"ui.dev.clients.general.public_key.cache.parsed_key_n", "ui.dev.clients.general.public_key.cache.parsed_key_n",
"n Preview", "N",
)} )}
</p> </p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]"> <p className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
{key.nPreview || "-"} {key.n || "-"}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -80,7 +80,7 @@ export type ClientDetailResponse = {
kty?: string; kty?: string;
use?: string; use?: string;
alg?: string; alg?: string;
nPreview?: string; n?: string;
}>; }>;
}; };
}; };

View File

@@ -149,8 +149,8 @@ test.describe("DevFront clients lifecycle", () => {
kty: "RSA", kty: "RSA",
use: "sig", use: "sig",
alg: "RS256", alg: "RS256",
nPreview: n:
"voVbHlo_UHkj...Hb8PiTCQ", "voVbHlo_UHkjtT7Q_8owyjZ2omE8n8mbGlpraZziStHPfe08q_RGiEXO6Pyiz42NVi-Yo0c7qiaqRwB4h9s5phpT2wwcUxnkrQeRhe7BpigInZPzpwq1hsaB2zyhE7zTRCC3hinGtFdVpNzTVKYKGPbXfeEXaRL3P838vi-_iB4IN3WQk_pAakUQvajL2H-vcWSMSNslMGPDZxobqE9MHSWocNXemrcmtCeE7ruUND0qHZOb8k-hHUBqsNoJ63WKdapzGYF6e2qgDRveYrjgOCBigZPi8npN0xStQ0YcrH_RxeTogsdRZ8SuXmLqavryVDnrT8czPkkJ-EHb8PiTCQ",
}, },
], ],
}, },
@@ -211,6 +211,7 @@ test.describe("DevFront clients lifecycle", () => {
page.getByText(/cached at|캐시됨|last refresh|마지막 갱신/i), page.getByText(/cached at|캐시됨|last refresh|마지막 갱신/i),
).toBeVisible(); ).toBeVisible();
await expect(page.getByText(/Parsed Keys|파싱된 키/i)).toBeVisible(); await expect(page.getByText(/Parsed Keys|파싱된 키/i)).toBeVisible();
await expect(page.getByText(/^KID$/i)).toBeVisible();
await expect(page.getByText("kid-1", { exact: true }).last()).toBeVisible(); await expect(page.getByText("kid-1", { exact: true }).last()).toBeVisible();
await expect( await expect(
page.getByText(/Allowed algorithms|허용 알고리즘/i), page.getByText(/Allowed algorithms|허용 알고리즘/i),
@@ -230,7 +231,10 @@ test.describe("DevFront clients lifecycle", () => {
await expect(page.getByText(algorithm, { exact: true }).last()).toBeVisible(); await expect(page.getByText(algorithm, { exact: true }).last()).toBeVisible();
} }
await expect( await expect(
page.getByText("voVbHlo_UHkj...Hb8PiTCQ", { exact: true }), page.getByText(
"voVbHlo_UHkjtT7Q_8owyjZ2omE8n8mbGlpraZziStHPfe08q_RGiEXO6Pyiz42NVi-Yo0c7qiaqRwB4h9s5phpT2wwcUxnkrQeRhe7BpigInZPzpwq1hsaB2zyhE7zTRCC3hinGtFdVpNzTVKYKGPbXfeEXaRL3P838vi-_iB4IN3WQk_pAakUQvajL2H-vcWSMSNslMGPDZxobqE9MHSWocNXemrcmtCeE7ruUND0qHZOb8k-hHUBqsNoJ63WKdapzGYF6e2qgDRveYrjgOCBigZPi8npN0xStQ0YcrH_RxeTogsdRZ8SuXmLqavryVDnrT8czPkkJ-EHb8PiTCQ",
{ exact: true },
),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByRole("button", { name: /refresh|새로고침/i }), page.getByRole("button", { name: /refresh|새로고침/i }),

View File

@@ -33,7 +33,7 @@ export type Client = {
kty?: string; kty?: string;
use?: string; use?: string;
alg?: string; alg?: string;
nPreview?: string; n?: string;
}>; }>;
}; };
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;