forked from baron/baron-sso
code-check 오류 수정
This commit is contained in:
@@ -49,6 +49,20 @@ interface ScopeItem {
|
||||
}
|
||||
|
||||
type SecurityProfile = "private" | "pkce";
|
||||
type TokenEndpointAuthMethod =
|
||||
| "none"
|
||||
| "client_secret_basic"
|
||||
| "private_key_jwt";
|
||||
|
||||
function isTokenEndpointAuthMethod(
|
||||
value: string,
|
||||
): value is TokenEndpointAuthMethod {
|
||||
return (
|
||||
value === "none" ||
|
||||
value === "client_secret_basic" ||
|
||||
value === "private_key_jwt"
|
||||
);
|
||||
}
|
||||
|
||||
function readMetadataString(
|
||||
metadata: Record<string, unknown>,
|
||||
@@ -96,17 +110,17 @@ function ClientGeneralPage() {
|
||||
const [status, setStatus] = useState<ClientStatus>("active");
|
||||
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
||||
const [redirectUris, setRedirectUris] = useState("");
|
||||
|
||||
|
||||
// Public Key Registration States
|
||||
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] = useState<
|
||||
"none" | "client_secret_basic" | "private_key_jwt"
|
||||
>("client_secret_basic");
|
||||
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
|
||||
useState<TokenEndpointAuthMethod>("client_secret_basic");
|
||||
const [jwksSource, setJwksSource] = useState<"uri" | "inline">("inline");
|
||||
const [jwksUri, setJwksUri] = useState("");
|
||||
const [jwksText, setJwksText] = useState("");
|
||||
const [requestObjectSigningAlg, setRequestObjectSigningAlg] = useState("RS256");
|
||||
const [requestObjectSigningAlg, setRequestObjectSigningAlg] =
|
||||
useState("RS256");
|
||||
const [headlessLoginEnabled, setHeadlessLoginEnabled] = useState(false);
|
||||
|
||||
|
||||
const [scopes, setScopes] = useState<ScopeItem[]>(() => [
|
||||
{
|
||||
id: "1",
|
||||
@@ -136,40 +150,55 @@ function ClientGeneralPage() {
|
||||
setStatus(client.status);
|
||||
setInitialStatus(client.status);
|
||||
|
||||
const savedAuthMethod = client.tokenEndpointAuthMethod || (client.type === "pkce" ? "none" : "client_secret_basic");
|
||||
setTokenEndpointAuthMethod(savedAuthMethod as any);
|
||||
|
||||
const savedAuthMethod =
|
||||
client.tokenEndpointAuthMethod ||
|
||||
(client.type === "pkce" ? "none" : "client_secret_basic");
|
||||
if (isTokenEndpointAuthMethod(savedAuthMethod)) {
|
||||
setTokenEndpointAuthMethod(savedAuthMethod);
|
||||
}
|
||||
|
||||
if (client.jwksUri) {
|
||||
setJwksUri(client.jwksUri);
|
||||
setJwksSource("uri");
|
||||
} else if (client.jwks) {
|
||||
setJwksText(typeof client.jwks === 'string' ? client.jwks : JSON.stringify(client.jwks, null, 2));
|
||||
setJwksText(
|
||||
typeof client.jwks === "string"
|
||||
? client.jwks
|
||||
: JSON.stringify(client.jwks, null, 2),
|
||||
);
|
||||
setJwksSource("inline");
|
||||
}
|
||||
|
||||
const metadata = client.metadata ?? {};
|
||||
if (typeof metadata.description === "string") setDescription(metadata.description);
|
||||
if (typeof metadata.description === "string")
|
||||
setDescription(metadata.description);
|
||||
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
|
||||
|
||||
|
||||
setHeadlessLoginEnabled(!!metadata.headless_login_enabled);
|
||||
|
||||
|
||||
// Fallbacks from metadata if top-level fields are empty
|
||||
if (!client.tokenEndpointAuthMethod) {
|
||||
const metaAuth = readMetadataString(metadata, "token_endpoint_auth_method");
|
||||
if (metaAuth === "none" || metaAuth === "client_secret_basic" || metaAuth === "private_key_jwt") {
|
||||
setTokenEndpointAuthMethod(metaAuth);
|
||||
}
|
||||
const metaAuth = readMetadataString(
|
||||
metadata,
|
||||
"token_endpoint_auth_method",
|
||||
);
|
||||
if (isTokenEndpointAuthMethod(metaAuth)) {
|
||||
setTokenEndpointAuthMethod(metaAuth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!client.jwksUri && !client.jwks) {
|
||||
const metaJwksUri = readMetadataString(metadata, "jwks_uri");
|
||||
if (metaJwksUri) {
|
||||
setJwksUri(metaJwksUri);
|
||||
setJwksSource("uri");
|
||||
}
|
||||
const metaJwksUri = readMetadataString(metadata, "jwks_uri");
|
||||
if (metaJwksUri) {
|
||||
setJwksUri(metaJwksUri);
|
||||
setJwksSource("uri");
|
||||
}
|
||||
}
|
||||
|
||||
const savedRequestObjectSigningAlg = readMetadataString(metadata, "request_object_signing_alg");
|
||||
|
||||
const savedRequestObjectSigningAlg = readMetadataString(
|
||||
metadata,
|
||||
"request_object_signing_alg",
|
||||
);
|
||||
if (savedRequestObjectSigningAlg) {
|
||||
setRequestObjectSigningAlg(savedRequestObjectSigningAlg);
|
||||
} else if (savedAuthMethod === "private_key_jwt") {
|
||||
@@ -191,12 +220,15 @@ function ClientGeneralPage() {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const securityProfile: SecurityProfile = clientType === "pkce" ? "pkce" : "private";
|
||||
const securityProfile: SecurityProfile =
|
||||
clientType === "pkce" ? "pkce" : "private";
|
||||
|
||||
const handleSecurityProfileChange = (profile: SecurityProfile) => {
|
||||
setClientType(profile);
|
||||
if (profile === "pkce") {
|
||||
setTokenEndpointAuthMethod(headlessLoginEnabled ? "private_key_jwt" : "none");
|
||||
setTokenEndpointAuthMethod(
|
||||
headlessLoginEnabled ? "private_key_jwt" : "none",
|
||||
);
|
||||
} else {
|
||||
setTokenEndpointAuthMethod("client_secret_basic");
|
||||
}
|
||||
@@ -261,15 +293,35 @@ function ClientGeneralPage() {
|
||||
if (headlessLoginEnabled) {
|
||||
if (jwksSource === "uri") {
|
||||
if (!trimmedJwksUri) {
|
||||
validationErrors.push(t("msg.dev.clients.general.public_key.validation.missing_jwks_uri", "JWKS URI를 입력해야 합니다."));
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.validation.missing_jwks_uri",
|
||||
"JWKS URI를 입력해야 합니다.",
|
||||
),
|
||||
);
|
||||
} else if (!isValidUrl(trimmedJwksUri)) {
|
||||
validationErrors.push(t("msg.dev.clients.general.public_key.validation.invalid_jwks_uri", "JWKS URI 형식이 올바르지 않습니다."));
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.validation.invalid_jwks_uri",
|
||||
"JWKS URI 형식이 올바르지 않습니다.",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (jwksSource === "inline") {
|
||||
if (!trimmedJwksText) {
|
||||
validationErrors.push(t("msg.dev.clients.general.public_key.validation.missing_jwks_inline", "공개키(JWKS 또는 SSH-RSA)를 입력해야 합니다."));
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.validation.missing_jwks_inline",
|
||||
"공개키(JWKS 또는 SSH-RSA)를 입력해야 합니다.",
|
||||
),
|
||||
);
|
||||
} else if (!isValidJson(trimmedJwksText)) {
|
||||
validationErrors.push(t("msg.dev.clients.general.public_key.validation.invalid_jwks_inline", "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다."));
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.validation.invalid_jwks_inline",
|
||||
"입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,9 +340,13 @@ function ClientGeneralPage() {
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
||||
|
||||
let finalJwks: any = undefined;
|
||||
if (tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "inline" && trimmedJwksText) {
|
||||
|
||||
let finalJwks: ClientUpsertRequest["jwks"];
|
||||
if (
|
||||
tokenEndpointAuthMethod === "private_key_jwt" &&
|
||||
jwksSource === "inline" &&
|
||||
trimmedJwksText
|
||||
) {
|
||||
try {
|
||||
finalJwks = JSON.parse(trimmedJwksText);
|
||||
} catch (e) {
|
||||
@@ -303,7 +359,10 @@ function ClientGeneralPage() {
|
||||
type: clientType,
|
||||
scopes: scopeNames,
|
||||
tokenEndpointAuthMethod,
|
||||
jwksUri: tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "uri" ? trimmedJwksUri : undefined,
|
||||
jwksUri:
|
||||
tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "uri"
|
||||
? trimmedJwksUri
|
||||
: undefined,
|
||||
jwks: finalJwks,
|
||||
metadata: {
|
||||
description,
|
||||
@@ -848,22 +907,32 @@ function ClientGeneralPage() {
|
||||
</span>
|
||||
|
||||
{securityProfile === "pkce" && (
|
||||
<div
|
||||
<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="trusted-rp-toggle">
|
||||
{t("ui.dev.clients.general.security.trusted_rp_enable", "Trusted RP (자체 로그인 UI 사용)")}
|
||||
<Label
|
||||
className="text-xs font-bold cursor-pointer"
|
||||
htmlFor="trusted-rp-toggle"
|
||||
>
|
||||
{t(
|
||||
"ui.dev.clients.general.security.trusted_rp_enable",
|
||||
"Trusted RP (자체 로그인 UI 사용)",
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{t("ui.dev.clients.general.security.trusted_rp_enable_help", "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.")}
|
||||
{t(
|
||||
"ui.dev.clients.general.security.trusted_rp_enable_help",
|
||||
"Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
<Switch
|
||||
id="trusted-rp-toggle"
|
||||
checked={headlessLoginEnabled}
|
||||
onCheckedChange={handleHeadlessToggle}
|
||||
checked={headlessLoginEnabled}
|
||||
onCheckedChange={handleHeadlessToggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -910,7 +979,10 @@ function ClientGeneralPage() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="default" className="bg-primary/20 text-primary border-primary/30">
|
||||
<Badge
|
||||
variant="default"
|
||||
className="bg-primary/20 text-primary border-primary/30"
|
||||
>
|
||||
{t("ui.common.enabled", "Enabled")}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -984,7 +1056,10 @@ function ClientGeneralPage() {
|
||||
{jwksSource === "uri" && (
|
||||
<div className="space-y-2 animate-in fade-in slide-in-from-top-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t("ui.dev.clients.general.public_key.jwks_uri", "JWKS URI")}
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.jwks_uri",
|
||||
"JWKS URI",
|
||||
)}
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -1007,14 +1082,20 @@ function ClientGeneralPage() {
|
||||
{jwksSource === "inline" && (
|
||||
<div className="space-y-2 animate-in fade-in slide-in-from-top-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t("ui.dev.clients.general.public_key.jwks_inline", "JWKS 또는 OpenSSH 공개키")}
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.jwks_inline",
|
||||
"JWKS 또는 OpenSSH 공개키",
|
||||
)}
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
rows={8}
|
||||
value={jwksText}
|
||||
onChange={(e) => setJwksText(e.target.value)}
|
||||
placeholder={t("ui.dev.clients.general.public_key.jwks_inline_placeholder", "JWKS (JSON) 또는 'ssh-rsa AAA...' 형식의 공개키를 붙여넣으세요.")}
|
||||
placeholder={t(
|
||||
"ui.dev.clients.general.public_key.jwks_inline_placeholder",
|
||||
"JWKS (JSON) 또는 'ssh-rsa AAA...' 형식의 공개키를 붙여넣으세요.",
|
||||
)}
|
||||
className="font-mono text-xs leading-tight"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -24,7 +24,7 @@ function toBase64Url(base64: string): string {
|
||||
function hexToBase64Url(hex: string): string {
|
||||
const binary = hex
|
||||
.match(/.{1,2}/g)
|
||||
?.map((byte) => String.fromCharCode(parseInt(byte, 16)))
|
||||
?.map((byte) => String.fromCharCode(Number.parseInt(byte, 16)))
|
||||
.join("");
|
||||
if (!binary) return "";
|
||||
return toBase64Url(btoa(binary));
|
||||
@@ -42,12 +42,12 @@ export function parsePemToJwk(pem: string): JWK | null {
|
||||
.replace(/-----END PUBLIC KEY-----/, "")
|
||||
.replace(/\s/g, "");
|
||||
|
||||
// In a real browser environment without heavy libraries,
|
||||
// we would need a full ASN.1 parser.
|
||||
// In a real browser environment without heavy libraries,
|
||||
// we would need a full ASN.1 parser.
|
||||
// For now, we recommend using JWKS or OpenSSH formats for reliability,
|
||||
// or we can hint the user that complex PEMs might fail.
|
||||
// However, we'll try to support a basic one.
|
||||
|
||||
|
||||
return null; // Placeholder: PEM parsing is complex without libs.
|
||||
} catch (e) {
|
||||
console.error("Failed to parse PEM", e);
|
||||
@@ -100,12 +100,12 @@ export function parseSshRsaToJwk(sshKey: string): JWK | null {
|
||||
}
|
||||
|
||||
function semanticsBase64Url(blob: string): string {
|
||||
// Ensure leading zero removal for BigInt representations if necessary
|
||||
let start = 0;
|
||||
while (start < blob.length && blob.charCodeAt(start) === 0) {
|
||||
start++;
|
||||
}
|
||||
return toBase64Url(btoa(blob.slice(start)));
|
||||
// Ensure leading zero removal for BigInt representations if necessary
|
||||
let start = 0;
|
||||
while (start < blob.length && blob.charCodeAt(start) === 0) {
|
||||
start++;
|
||||
}
|
||||
return toBase64Url(btoa(blob.slice(start)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +114,7 @@ function semanticsBase64Url(blob: string): string {
|
||||
*/
|
||||
export function tryConvertToJwks(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
|
||||
|
||||
// 1. If it looks like JSON, return as is (validation happens in component)
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
return trimmed;
|
||||
@@ -130,9 +130,9 @@ export function tryConvertToJwks(input: string): string {
|
||||
|
||||
// 3. PEM (Simplified check)
|
||||
if (trimmed.includes("BEGIN PUBLIC KEY")) {
|
||||
// For PEM, we suggest the user uses JWKS or SSH-RSA for now
|
||||
// as JS doesn't have a built-in ASN1 parser and we want to avoid heavy deps.
|
||||
return trimmed;
|
||||
// For PEM, we suggest the user uses JWKS or SSH-RSA for now
|
||||
// as JS doesn't have a built-in ASN1 parser and we want to avoid heavy deps.
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
|
||||
Reference in New Issue
Block a user