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:
@@ -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 캐시 상태와 최근 동기화 결과를 나타냅니다.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export type ClientDetailResponse = {
|
|||||||
kty?: string;
|
kty?: string;
|
||||||
use?: string;
|
use?: string;
|
||||||
alg?: string;
|
alg?: string;
|
||||||
nPreview?: string;
|
n?: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user