forked from baron/baron-sso
fix(headless-login): simplify jwks policy checks
This commit is contained in:
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Info,
|
||||
Plus,
|
||||
Save,
|
||||
Shield,
|
||||
@@ -68,6 +69,18 @@ const HEADLESS_LOGIN_ALLOWED_ALGORITHMS = [
|
||||
"EdDSA",
|
||||
] as const;
|
||||
|
||||
const HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET = new Set<string>(
|
||||
HEADLESS_LOGIN_ALLOWED_ALGORITHMS,
|
||||
);
|
||||
|
||||
function formatHeadlessParsedKeyLabel(kid: string | undefined, index: number): string {
|
||||
const trimmedKid = kid?.trim();
|
||||
if (trimmedKid) {
|
||||
return trimmedKid;
|
||||
}
|
||||
return `key #${index + 1}`;
|
||||
}
|
||||
|
||||
function isTokenEndpointAuthMethod(
|
||||
value: string,
|
||||
): value is TokenEndpointAuthMethod {
|
||||
@@ -126,8 +139,6 @@ function ClientGeneralPage() {
|
||||
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
|
||||
useState<TokenEndpointAuthMethod>("client_secret_basic");
|
||||
const [jwksUri, setJwksUri] = useState("");
|
||||
const [requestObjectSigningAlg, setRequestObjectSigningAlg] =
|
||||
useState("RS256");
|
||||
const [headlessLoginEnabled, setHeadlessLoginEnabled] = useState(false);
|
||||
|
||||
const [scopes, setScopes] = useState<ScopeItem[]>(() => [
|
||||
@@ -209,16 +220,6 @@ function ClientGeneralPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const savedRequestObjectSigningAlg = readMetadataString(
|
||||
metadata,
|
||||
"request_object_signing_alg",
|
||||
);
|
||||
if (savedRequestObjectSigningAlg) {
|
||||
setRequestObjectSigningAlg(savedRequestObjectSigningAlg);
|
||||
} else if (savedAuthMethod === "private_key_jwt") {
|
||||
setRequestObjectSigningAlg("RS256");
|
||||
}
|
||||
|
||||
const savedScopes = metadata.structured_scopes as ScopeItem[] | undefined;
|
||||
if (savedScopes && Array.isArray(savedScopes)) {
|
||||
setScopes(savedScopes);
|
||||
@@ -252,9 +253,6 @@ function ClientGeneralPage() {
|
||||
setHeadlessLoginEnabled(enabled);
|
||||
if (clientType === "pkce") {
|
||||
setTokenEndpointAuthMethod(enabled ? "private_key_jwt" : "none");
|
||||
if (enabled && requestObjectSigningAlg.trim() === "") {
|
||||
setRequestObjectSigningAlg("RS256");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -299,7 +297,40 @@ function ClientGeneralPage() {
|
||||
|
||||
const validationErrors: string[] = [];
|
||||
const trimmedJwksUri = jwksUri.trim();
|
||||
const trimmedRequestObjectSigningAlg = requestObjectSigningAlg.trim();
|
||||
const currentHeadlessJwksCache = data?.headlessJwksCache;
|
||||
const parsedKeysForCurrentJwksUri =
|
||||
headlessLoginEnabled &&
|
||||
trimmedJwksUri !== "" &&
|
||||
currentHeadlessJwksCache?.jwksUri === trimmedJwksUri
|
||||
? currentHeadlessJwksCache.parsedKeys ?? []
|
||||
: [];
|
||||
const unsupportedParsedAlgorithms = parsedKeysForCurrentJwksUri
|
||||
.map((key, index) => ({
|
||||
alg: key.alg?.trim() ?? "",
|
||||
label: formatHeadlessParsedKeyLabel(key.kid, index),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.alg !== "" &&
|
||||
!HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(entry.alg),
|
||||
);
|
||||
const missingParsedAlgorithms = parsedKeysForCurrentJwksUri
|
||||
.map((key, index) => ({
|
||||
alg: key.alg?.trim() ?? "",
|
||||
label: formatHeadlessParsedKeyLabel(key.kid, index),
|
||||
}))
|
||||
.filter((entry) => entry.alg === "");
|
||||
const unsupportedParsedAlgorithmSummary = unsupportedParsedAlgorithms
|
||||
.map((entry) => `${entry.label}: ${entry.alg}`)
|
||||
.join(", ");
|
||||
const missingParsedAlgorithmSummary = missingParsedAlgorithms
|
||||
.map((entry) => entry.label)
|
||||
.join(", ");
|
||||
const allowedHeadlessAlgorithmsTooltip = t(
|
||||
"msg.dev.clients.general.public_key.allowed_algorithms_tooltip",
|
||||
"허용 알고리즘: {{algorithms}}",
|
||||
{ algorithms: HEADLESS_LOGIN_ALLOWED_ALGORITHMS.join(", ") },
|
||||
);
|
||||
|
||||
if (headlessLoginEnabled) {
|
||||
if (!trimmedJwksUri) {
|
||||
@@ -317,19 +348,27 @@ function ClientGeneralPage() {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (trimmedRequestObjectSigningAlg === "") {
|
||||
if (unsupportedParsedAlgorithms.length > 0) {
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.validation.headless_requires_alg",
|
||||
"Request Object Signing Algorithm (예: RS256)을 입력해야 합니다.",
|
||||
"msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms",
|
||||
"JWKS에 지원하지 않는 알고리즘이 있습니다: {{details}}",
|
||||
{ details: unsupportedParsedAlgorithmSummary },
|
||||
),
|
||||
);
|
||||
}
|
||||
if (missingParsedAlgorithms.length > 0) {
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.validation.missing_parsed_algorithms",
|
||||
"JWKS에 알고리즘(`alg`)이 선언되지 않은 키가 있습니다: {{details}}",
|
||||
{ details: missingParsedAlgorithmSummary },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hasValidationErrors = validationErrors.length > 0;
|
||||
const currentHeadlessJwksCache = data?.headlessJwksCache;
|
||||
|
||||
const refreshHeadlessJwksCacheMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -418,7 +457,6 @@ function ClientGeneralPage() {
|
||||
logo_url: logoUrl,
|
||||
structured_scopes: scopes,
|
||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||
request_object_signing_alg: trimmedRequestObjectSigningAlg,
|
||||
headless_login_enabled: headlessLoginEnabled,
|
||||
headless_token_endpoint_auth_method:
|
||||
clientType === "pkce" && headlessLoginEnabled
|
||||
@@ -1046,66 +1084,31 @@ function ClientGeneralPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3 rounded-xl border border-border bg-muted/5 p-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold" htmlFor="request-object-signing-alg">
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.request_object_alg",
|
||||
"Request Object Signing Algorithm",
|
||||
)}
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="request-object-signing-alg"
|
||||
value={requestObjectSigningAlg}
|
||||
onChange={(e) => setRequestObjectSigningAlg(e.target.value)}
|
||||
placeholder={t(
|
||||
"ui.dev.clients.general.public_key.request_object_alg_placeholder",
|
||||
"예: RS256",
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.request_object_alg_help",
|
||||
"Headless Login을 사용할 때 JAR(Request Object) 서명 검증에 사용할 알고리즘을 명시합니다.",
|
||||
)}
|
||||
</p>
|
||||
<div className="space-y-2 rounded-lg border border-border bg-background/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-semibold" htmlFor="jwks-uri">
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.allowed_algorithms",
|
||||
"Allowed Algorithms",
|
||||
"ui.dev.clients.general.public_key.jwks_uri",
|
||||
"JWKS URI",
|
||||
)}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{HEADLESS_LOGIN_ALLOWED_ALGORITHMS.map((algorithm) => (
|
||||
<Badge
|
||||
key={algorithm}
|
||||
variant="outline"
|
||||
className="font-mono text-[11px]"
|
||||
>
|
||||
{algorithm}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.allowed_algorithms_help",
|
||||
"Headless Login JAR 검증은 이 목록에 있는 서명 알고리즘만 허용합니다.",
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground"
|
||||
title={allowedHeadlessAlgorithmsTooltip}
|
||||
aria-label={t(
|
||||
"ui.dev.clients.general.public_key.allowed_algorithms_info",
|
||||
"Headless Login 허용 알고리즘 정보",
|
||||
)}
|
||||
</p>
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<Label className="text-sm font-semibold" htmlFor="jwks-uri">
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.jwks_uri",
|
||||
"JWKS URI",
|
||||
)}
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="jwks-uri"
|
||||
value={jwksUri}
|
||||
@@ -1297,6 +1300,35 @@ function ClientGeneralPage() {
|
||||
{currentHeadlessJwksCache.lastError || "-"}
|
||||
</p>
|
||||
</div>
|
||||
{(unsupportedParsedAlgorithms.length > 0 ||
|
||||
missingParsedAlgorithms.length > 0) && (
|
||||
<div className="space-y-2 rounded-lg border border-destructive/40 bg-destructive/5 p-3 md:col-span-2">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
{unsupportedParsedAlgorithms.length > 0
|
||||
? t(
|
||||
"msg.dev.clients.general.public_key.cache.unsupported_algorithms_title",
|
||||
"지원하지 않는 알고리즘이 감지되었습니다.",
|
||||
)
|
||||
: t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithms_title",
|
||||
"알고리즘이 선언되지 않았습니다.",
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-destructive">
|
||||
{unsupportedParsedAlgorithms.length > 0
|
||||
? t(
|
||||
"msg.dev.clients.general.public_key.cache.unsupported_algorithms_help",
|
||||
"저장 전 JWKS를 수정해 주세요: {{details}}",
|
||||
{ details: unsupportedParsedAlgorithmSummary },
|
||||
)
|
||||
: t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithms_help",
|
||||
"저장 전 JWKS 각 키에 `alg`를 명시해 주세요: {{details}}",
|
||||
{ details: missingParsedAlgorithmSummary },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-3 md:col-span-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
@@ -1314,58 +1346,101 @@ function ClientGeneralPage() {
|
||||
</div>
|
||||
{currentHeadlessJwksCache.parsedKeys?.length ? (
|
||||
<div className="space-y-3">
|
||||
{currentHeadlessJwksCache.parsedKeys.map((key, index) => (
|
||||
<div
|
||||
key={`${key.kid || "key"}-${index}`}
|
||||
className="rounded-xl border border-border bg-muted/30 p-3"
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KID
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.kid || "-"}
|
||||
</p>
|
||||
{currentHeadlessJwksCache.parsedKeys.map((key, index) => {
|
||||
const normalizedAlgorithm = key.alg?.trim() ?? "";
|
||||
const isMissingAlgorithm =
|
||||
normalizedAlgorithm === "";
|
||||
const isUnsupportedAlgorithm =
|
||||
!isMissingAlgorithm &&
|
||||
!HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(
|
||||
normalizedAlgorithm,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${key.kid || "key"}-${index}`}
|
||||
className={cn(
|
||||
"rounded-xl border bg-muted/30 p-3",
|
||||
isUnsupportedAlgorithm || isMissingAlgorithm
|
||||
? "border-destructive/50 bg-destructive/5"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KID
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.kid || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KTY
|
||||
</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={cn(
|
||||
"break-all rounded-lg border bg-background px-3 py-2 font-mono text-[11px]",
|
||||
isUnsupportedAlgorithm || isMissingAlgorithm
|
||||
? "border-destructive/50 text-destructive"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
{key.alg ||
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithm_badge",
|
||||
"알고리즘 미선언",
|
||||
)}
|
||||
</p>
|
||||
{isMissingAlgorithm && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithm_reason",
|
||||
"이 키는 `alg`가 비어 있어서 저장할 수 없습니다.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{isUnsupportedAlgorithm && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason",
|
||||
"이 알고리즘은 Headless Login에서 지원되지 않습니다.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="mt-3 space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KTY
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.cache.parsed_key_n",
|
||||
"N",
|
||||
)}
|
||||
</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 className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
|
||||
{key.n || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.cache.parsed_key_n",
|
||||
"N",
|
||||
)}
|
||||
</p>
|
||||
<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.n || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-border px-4 py-5 text-sm text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user