diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go
index fe93a76a..94baa6b1 100644
--- a/backend/internal/handler/dev_handler.go
+++ b/backend/internal/handler/dev_handler.go
@@ -143,7 +143,7 @@ type clientUpsertRequest struct {
ResponseTypes *[]string `json:"responseTypes"`
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
JwksUri *string `json:"jwksUri"`
- Jwks interface{} `json:"jwks"`
+ Jwks interface{} `json:"jwks"`
Metadata *map[string]interface{} `json:"metadata"`
}
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx
index 85762cea..fce5a6ac 100644
--- a/devfront/src/features/clients/ClientGeneralPage.tsx
+++ b/devfront/src/features/clients/ClientGeneralPage.tsx
@@ -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,
@@ -96,17 +110,17 @@ function ClientGeneralPage() {
const [status, setStatus] = useState("active");
const [initialStatus, setInitialStatus] = useState("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("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(() => [
{
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() {
{securityProfile === "pkce" && (
- e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
>
-
-
)}
@@ -910,7 +979,10 @@ function ClientGeneralPage() {
)}
-
+
{t("ui.common.enabled", "Enabled")}
@@ -984,7 +1056,10 @@ function ClientGeneralPage() {
{jwksSource === "uri" && (