forked from baron/baron-sso
Merge pull request 'feature/df-trusted-rp' (#467) from feature/df-trusted-rp into dev
Reviewed-on: baron/baron-sso#467
This commit is contained in:
@@ -12,9 +12,36 @@ type HydraClient struct {
|
|||||||
ResponseTypes []string `json:"response_types,omitempty"`
|
ResponseTypes []string `json:"response_types,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||||
|
JWKSUri string `json:"jwks_uri,omitempty"`
|
||||||
|
JWKS interface{} `json:"jwks,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *HydraClient) IsTrustedRP() bool {
|
||||||
|
// A Trusted RP must have a public key registered (URI or Inline)
|
||||||
|
// and use private_key_jwt for token endpoint authentication.
|
||||||
|
hasPublicKey := c.JWKSUri != "" || c.JWKS != nil
|
||||||
|
isPrivateKeyJwt := c.TokenEndpointAuthMethod == "private_key_jwt"
|
||||||
|
return hasPublicKey && isPrivateKeyJwt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HydraClient) IsHeadlessLoginEnabled() bool {
|
||||||
|
if !c.IsTrustedRP() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.Metadata == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val, ok := c.Metadata["headless_login_enabled"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if b, ok := val.(bool); ok {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type HydraConsentRequest struct {
|
type HydraConsentRequest struct {
|
||||||
Challenge string `json:"challenge"`
|
Challenge string `json:"challenge"`
|
||||||
RequestedScope []string `json:"requested_scope"`
|
RequestedScope []string `json:"requested_scope"`
|
||||||
|
|||||||
60
backend/internal/domain/hydra_models_test.go
Normal file
60
backend/internal/domain/hydra_models_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHydraClient_TrustedRPFlags(t *testing.T) {
|
||||||
|
t.Run("inline jwks with private_key_jwt and headless enabled", func(t *testing.T) {
|
||||||
|
client := HydraClient{
|
||||||
|
TokenEndpointAuthMethod: "private_key_jwt",
|
||||||
|
JWKS: map[string]any{
|
||||||
|
"keys": []map[string]any{{
|
||||||
|
"kty": "RSA",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.IsTrustedRP() {
|
||||||
|
t.Fatalf("expected trusted rp")
|
||||||
|
}
|
||||||
|
if !client.IsHeadlessLoginEnabled() {
|
||||||
|
t.Fatalf("expected headless login enabled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jwks uri without private_key_jwt is not trusted", func(t *testing.T) {
|
||||||
|
client := HydraClient{
|
||||||
|
TokenEndpointAuthMethod: "none",
|
||||||
|
JWKSUri: "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.IsTrustedRP() {
|
||||||
|
t.Fatalf("expected untrusted rp")
|
||||||
|
}
|
||||||
|
if client.IsHeadlessLoginEnabled() {
|
||||||
|
t.Fatalf("expected headless login disabled when client is not trusted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("trusted rp without boolean metadata flag is not headless enabled", func(t *testing.T) {
|
||||||
|
client := HydraClient{
|
||||||
|
TokenEndpointAuthMethod: "private_key_jwt",
|
||||||
|
JWKSUri: "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headless_login_enabled": "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.IsTrustedRP() {
|
||||||
|
t.Fatalf("expected trusted rp")
|
||||||
|
}
|
||||||
|
if client.IsHeadlessLoginEnabled() {
|
||||||
|
t.Fatalf("expected headless login disabled for non-bool metadata")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -81,15 +81,18 @@ type devStatsResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type clientSummary struct {
|
type clientSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||||
RedirectURIs []string `json:"redirectUris"`
|
RedirectURIs []string `json:"redirectUris"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
ClientSecret string `json:"clientSecret,omitempty"`
|
ClientSecret string `json:"clientSecret,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
|
||||||
|
JwksUri string `json:"jwksUri,omitempty"`
|
||||||
|
Jwks interface{} `json:"jwks,omitempty"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientListResponse struct {
|
type clientListResponse struct {
|
||||||
@@ -139,6 +142,8 @@ type clientUpsertRequest struct {
|
|||||||
GrantTypes *[]string `json:"grantTypes"`
|
GrantTypes *[]string `json:"grantTypes"`
|
||||||
ResponseTypes *[]string `json:"responseTypes"`
|
ResponseTypes *[]string `json:"responseTypes"`
|
||||||
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
|
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
|
||||||
|
JwksUri *string `json:"jwksUri"`
|
||||||
|
Jwks interface{} `json:"jwks"`
|
||||||
Metadata *map[string]interface{} `json:"metadata"`
|
Metadata *map[string]interface{} `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -895,6 +900,8 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
ResponseTypes: responseTypes,
|
ResponseTypes: responseTypes,
|
||||||
Scope: strings.Join(scopes, " "),
|
Scope: strings.Join(scopes, " "),
|
||||||
TokenEndpointAuthMethod: tokenAuthMethod,
|
TokenEndpointAuthMethod: tokenAuthMethod,
|
||||||
|
JWKSUri: valueOr(req.JwksUri, ""),
|
||||||
|
JWKS: req.Jwks,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,8 +1053,13 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
|
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
|
||||||
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
|
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
|
||||||
TokenEndpointAuthMethod: resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod),
|
TokenEndpointAuthMethod: resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod),
|
||||||
|
JWKSUri: valueOr(req.JwksUri, current.JWKSUri),
|
||||||
|
JWKS: req.Jwks,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
if req.Jwks == nil {
|
||||||
|
updated.JWKS = current.JWKS
|
||||||
|
}
|
||||||
if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
|
if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
|
||||||
return errorJSON(c, fiber.StatusForbidden, err.Error())
|
return errorJSON(c, fiber.StatusForbidden, err.Error())
|
||||||
}
|
}
|
||||||
@@ -1640,15 +1652,18 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return clientSummary{
|
return clientSummary{
|
||||||
ID: client.ClientID,
|
ID: client.ClientID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: clientType,
|
Type: clientType,
|
||||||
Status: status,
|
Status: status,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
RedirectURIs: client.RedirectURIs,
|
RedirectURIs: client.RedirectURIs,
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
Metadata: client.Metadata,
|
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
|
||||||
|
JwksUri: client.JWKSUri,
|
||||||
|
Jwks: client.JWKS,
|
||||||
|
Metadata: client.Metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -610,6 +611,156 @@ func TestDevHandler_NoAuditNoAction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateClient_TrustedRPPayloadMapping(t *testing.T) {
|
||||||
|
var captured domain.HydraClient
|
||||||
|
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Method == http.MethodPost && r.URL.Path == "/clients" {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = json.Unmarshal(body, &captured)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return httpJSONAny(r, http.StatusCreated, map[string]any{
|
||||||
|
"client_id": captured.ClientID,
|
||||||
|
"client_name": captured.ClientName,
|
||||||
|
"redirect_uris": captured.RedirectURIs,
|
||||||
|
"grant_types": captured.GrantTypes,
|
||||||
|
"response_types": captured.ResponseTypes,
|
||||||
|
"scope": captured.Scope,
|
||||||
|
"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
|
||||||
|
"jwks": captured.JWKS,
|
||||||
|
"metadata": captured.Metadata,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
h := &DevHandler{
|
||||||
|
Hydra: &service.HydraAdminService{
|
||||||
|
AdminURL: "http://hydra.test",
|
||||||
|
PublicURL: "http://hydra.public",
|
||||||
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
|
},
|
||||||
|
Keto: new(devMockKetoService),
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Post("/api/v1/dev/clients", h.CreateClient)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "Trusted RP App",
|
||||||
|
"type": "pkce",
|
||||||
|
"redirectUris": []string{"https://rp.example.com/callback"},
|
||||||
|
"scopes": []string{"openid", "profile"},
|
||||||
|
"tokenEndpointAuthMethod": "private_key_jwt",
|
||||||
|
"jwks": map[string]any{
|
||||||
|
"keys": []map[string]any{{
|
||||||
|
"kty": "RSA",
|
||||||
|
"alg": "RS256",
|
||||||
|
"n": "AQIDBAUGBw",
|
||||||
|
"e": "AQAB",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
"request_object_signing_alg": "RS256",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
|
assert.NotNil(t, captured.JWKS)
|
||||||
|
assert.True(t, captured.IsTrustedRP())
|
||||||
|
assert.True(t, captured.IsHeadlessLoginEnabled())
|
||||||
|
assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
|
||||||
|
assert.Equal(t, "RS256", captured.Metadata["request_object_signing_alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateClient_TrustedRPPayloadMapping(t *testing.T) {
|
||||||
|
var captured domain.HydraClient
|
||||||
|
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-trusted" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"client_id": "client-trusted",
|
||||||
|
"client_name": "Trusted Before",
|
||||||
|
"redirect_uris": []string{"https://before.example.com/callback"},
|
||||||
|
"grant_types": []string{"authorization_code", "refresh_token"},
|
||||||
|
"response_types": []string{"code"},
|
||||||
|
"scope": "openid profile",
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"status": "active",
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodPut && r.URL.Path == "/clients/client-trusted" {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = json.Unmarshal(body, &captured)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"client_id": captured.ClientID,
|
||||||
|
"client_name": captured.ClientName,
|
||||||
|
"redirect_uris": captured.RedirectURIs,
|
||||||
|
"grant_types": captured.GrantTypes,
|
||||||
|
"response_types": captured.ResponseTypes,
|
||||||
|
"scope": captured.Scope,
|
||||||
|
"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
|
||||||
|
"jwks_uri": captured.JWKSUri,
|
||||||
|
"metadata": captured.Metadata,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
h := &DevHandler{
|
||||||
|
Hydra: &service.HydraAdminService{
|
||||||
|
AdminURL: "http://hydra.test",
|
||||||
|
PublicURL: "http://hydra.public",
|
||||||
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
|
},
|
||||||
|
Keto: new(devMockKetoService),
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Put("/api/v1/dev/clients/:id", h.UpdateClient)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "Trusted After",
|
||||||
|
"type": "pkce",
|
||||||
|
"tokenEndpointAuthMethod": "private_key_jwt",
|
||||||
|
"jwksUri": "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
"request_object_signing_alg": "RS256",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-trusted", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
|
||||||
|
assert.True(t, captured.IsTrustedRP())
|
||||||
|
assert.True(t, captured.IsHeadlessLoginEnabled())
|
||||||
|
assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
|
||||||
|
}
|
||||||
|
|
||||||
func TestListAuditLogs_TenantMemberForbidden(t *testing.T) {
|
func TestListAuditLogs_TenantMemberForbidden(t *testing.T) {
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
Hydra: &service.HydraAdminService{AdminURL: "http://hydra.test"},
|
Hydra: &service.HydraAdminService{AdminURL: "http://hydra.test"},
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import type {
|
|||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
import { tryConvertToJwks } from "../../lib/keyUtils";
|
||||||
|
|
||||||
interface ScopeItem {
|
interface ScopeItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -47,6 +48,49 @@ interface ScopeItem {
|
|||||||
mandatory: boolean;
|
mandatory: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>,
|
||||||
|
key: string,
|
||||||
|
): string {
|
||||||
|
const value = metadata[key];
|
||||||
|
return typeof value === "string" ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUrl(value: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.protocol === "https:" || url.protocol === "http:";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidJson(value: string): boolean {
|
||||||
|
if (!value.trim()) return false;
|
||||||
|
try {
|
||||||
|
JSON.parse(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ClientGeneralPage() {
|
function ClientGeneralPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -66,6 +110,17 @@ function ClientGeneralPage() {
|
|||||||
const [status, setStatus] = useState<ClientStatus>("active");
|
const [status, setStatus] = useState<ClientStatus>("active");
|
||||||
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
||||||
const [redirectUris, setRedirectUris] = useState("");
|
const [redirectUris, setRedirectUris] = useState("");
|
||||||
|
|
||||||
|
// Public Key Registration States
|
||||||
|
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 [headlessLoginEnabled, setHeadlessLoginEnabled] = useState(false);
|
||||||
|
|
||||||
const [scopes, setScopes] = useState<ScopeItem[]>(() => [
|
const [scopes, setScopes] = useState<ScopeItem[]>(() => [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
@@ -95,12 +150,61 @@ function ClientGeneralPage() {
|
|||||||
setStatus(client.status);
|
setStatus(client.status);
|
||||||
setInitialStatus(client.status);
|
setInitialStatus(client.status);
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
setJwksSource("inline");
|
||||||
|
}
|
||||||
|
|
||||||
const metadata = client.metadata ?? {};
|
const metadata = client.metadata ?? {};
|
||||||
if (typeof metadata.description === "string")
|
if (typeof metadata.description === "string")
|
||||||
setDescription(metadata.description);
|
setDescription(metadata.description);
|
||||||
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
|
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
|
||||||
|
|
||||||
// Metadata에 저장된 구조화된 scope 정보가 있으면 사용, 없으면 기본 scopes 문자열에서 생성
|
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 (isTokenEndpointAuthMethod(metaAuth)) {
|
||||||
|
setTokenEndpointAuthMethod(metaAuth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.jwksUri && !client.jwks) {
|
||||||
|
const metaJwksUri = readMetadataString(metadata, "jwks_uri");
|
||||||
|
if (metaJwksUri) {
|
||||||
|
setJwksUri(metaJwksUri);
|
||||||
|
setJwksSource("uri");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
const savedScopes = metadata.structured_scopes as ScopeItem[] | undefined;
|
||||||
if (savedScopes && Array.isArray(savedScopes)) {
|
if (savedScopes && Array.isArray(savedScopes)) {
|
||||||
setScopes(savedScopes);
|
setScopes(savedScopes);
|
||||||
@@ -116,6 +220,30 @@ function ClientGeneralPage() {
|
|||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const securityProfile: SecurityProfile =
|
||||||
|
clientType === "pkce" ? "pkce" : "private";
|
||||||
|
|
||||||
|
const handleSecurityProfileChange = (profile: SecurityProfile) => {
|
||||||
|
setClientType(profile);
|
||||||
|
if (profile === "pkce") {
|
||||||
|
setTokenEndpointAuthMethod(
|
||||||
|
headlessLoginEnabled ? "private_key_jwt" : "none",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTokenEndpointAuthMethod("client_secret_basic");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeadlessToggle = (enabled: boolean) => {
|
||||||
|
setHeadlessLoginEnabled(enabled);
|
||||||
|
if (clientType === "pkce") {
|
||||||
|
setTokenEndpointAuthMethod(enabled ? "private_key_jwt" : "none");
|
||||||
|
if (enabled && requestObjectSigningAlg.trim() === "") {
|
||||||
|
setRequestObjectSigningAlg("RS256");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const addScope = () => {
|
const addScope = () => {
|
||||||
const newId = String(Date.now());
|
const newId = String(Date.now());
|
||||||
setScopes([
|
setScopes([
|
||||||
@@ -155,21 +283,97 @@ function ClientGeneralPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert on blur or change if desired, here we try to convert before validation
|
||||||
|
const finalJwksText = tryConvertToJwks(jwksText);
|
||||||
|
const validationErrors: string[] = [];
|
||||||
|
const trimmedJwksUri = jwksUri.trim();
|
||||||
|
const trimmedJwksText = finalJwksText.trim();
|
||||||
|
const trimmedRequestObjectSigningAlg = requestObjectSigningAlg.trim();
|
||||||
|
|
||||||
|
if (headlessLoginEnabled) {
|
||||||
|
if (jwksSource === "uri") {
|
||||||
|
if (!trimmedJwksUri) {
|
||||||
|
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 형식이 올바르지 않습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (jwksSource === "inline") {
|
||||||
|
if (!trimmedJwksText) {
|
||||||
|
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'로 시작해야 합니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedRequestObjectSigningAlg === "") {
|
||||||
|
validationErrors.push(
|
||||||
|
t(
|
||||||
|
"msg.dev.clients.general.public_key.validation.headless_requires_alg",
|
||||||
|
"Request Object Signing Algorithm (예: RS256)을 입력해야 합니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidationErrors = validationErrors.length > 0;
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
||||||
|
|
||||||
|
let finalJwks: ClientUpsertRequest["jwks"];
|
||||||
|
if (
|
||||||
|
tokenEndpointAuthMethod === "private_key_jwt" &&
|
||||||
|
jwksSource === "inline" &&
|
||||||
|
trimmedJwksText
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
finalJwks = JSON.parse(trimmedJwksText);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Invalid Public Key Format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload: ClientUpsertRequest = {
|
const payload: ClientUpsertRequest = {
|
||||||
name,
|
name,
|
||||||
type: clientType,
|
type: clientType,
|
||||||
scopes: scopeNames,
|
scopes: scopeNames,
|
||||||
|
tokenEndpointAuthMethod,
|
||||||
|
jwksUri:
|
||||||
|
tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "uri"
|
||||||
|
? trimmedJwksUri
|
||||||
|
: undefined,
|
||||||
|
jwks: finalJwks,
|
||||||
metadata: {
|
metadata: {
|
||||||
description,
|
description,
|
||||||
logo_url: logoUrl,
|
logo_url: logoUrl,
|
||||||
structured_scopes: scopes, // 향후 보존을 위해 metadata에 저장
|
structured_scopes: scopes,
|
||||||
|
token_endpoint_auth_method: tokenEndpointAuthMethod,
|
||||||
|
request_object_signing_alg: trimmedRequestObjectSigningAlg,
|
||||||
|
headless_login_enabled: headlessLoginEnabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 생성 시에는 Redirect URIs를 포함해서 전송
|
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
payload.status = status;
|
payload.status = status;
|
||||||
payload.redirectUris = redirectUris
|
payload.redirectUris = redirectUris
|
||||||
@@ -179,8 +383,6 @@ function ClientGeneralPage() {
|
|||||||
return createClient(payload);
|
return createClient(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 수정 시에는 Redirect URIs는 별도 탭에서 관리하고,
|
|
||||||
// status는 전용 PATCH API로 처리해서 감사로그 액션을 분리한다.
|
|
||||||
const updated = await updateClient(clientId as string, payload);
|
const updated = await updateClient(clientId as string, payload);
|
||||||
if (status !== initialStatus) {
|
if (status !== initialStatus) {
|
||||||
await updateClientStatus(clientId as string, status);
|
await updateClientStatus(clientId as string, status);
|
||||||
@@ -271,6 +473,12 @@ function ClientGeneralPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publicKeyStatusTone = headlessLoginEnabled
|
||||||
|
? hasValidationErrors
|
||||||
|
? "border-destructive/40 bg-destructive/5"
|
||||||
|
: "border-primary/30 bg-primary/5"
|
||||||
|
: "border-border bg-muted/20";
|
||||||
|
|
||||||
const displayName = isCreate
|
const displayName = isCreate
|
||||||
? t("ui.dev.clients.general.display_new", "새 클라이언트")
|
? t("ui.dev.clients.general.display_new", "새 클라이언트")
|
||||||
: data?.client?.name || data?.client?.id;
|
: data?.client?.name || data?.client?.id;
|
||||||
@@ -472,7 +680,7 @@ function ClientGeneralPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Scopes (Moved up and upgraded) */}
|
{/* 2. Scopes */}
|
||||||
<Card className="glass-panel">
|
<Card className="glass-panel">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -497,7 +705,6 @@ function ClientGeneralPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Create 모드일 때만 Redirect URIs 입력 필드 표시 */}
|
|
||||||
{isCreate && (
|
{isCreate && (
|
||||||
<div className="space-y-2 border-b border-border pb-6 mb-6">
|
<div className="space-y-2 border-b border-border pb-6 mb-6">
|
||||||
<Label className="text-sm font-semibold">
|
<Label className="text-sm font-semibold">
|
||||||
@@ -622,7 +829,7 @@ function ClientGeneralPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 3. Security Settings (Moved down) */}
|
{/* 3. Security Settings */}
|
||||||
<Card className="glass-panel">
|
<Card className="glass-panel">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-xl font-bold">
|
<CardTitle className="text-xl font-bold">
|
||||||
@@ -640,7 +847,7 @@ function ClientGeneralPage() {
|
|||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
||||||
clientType === "private"
|
securityProfile === "private"
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: "border-border bg-card hover:border-muted-foreground/40",
|
: "border-border bg-card hover:border-muted-foreground/40",
|
||||||
)}
|
)}
|
||||||
@@ -648,9 +855,9 @@ function ClientGeneralPage() {
|
|||||||
<input
|
<input
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="client-type"
|
name="security-profile"
|
||||||
checked={clientType === "private"}
|
checked={securityProfile === "private"}
|
||||||
onChange={() => setClientType("private")}
|
onChange={() => handleSecurityProfileChange("private")}
|
||||||
/>
|
/>
|
||||||
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
||||||
<Shield className="h-4 w-4 text-primary" />
|
<Shield className="h-4 w-4 text-primary" />
|
||||||
@@ -666,14 +873,14 @@ function ClientGeneralPage() {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute right-4 top-4 text-primary">
|
<span className="absolute right-4 top-4 text-primary">
|
||||||
{clientType === "private" ? "✓" : ""}
|
{securityProfile === "private" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
||||||
clientType === "pkce"
|
securityProfile === "pkce"
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: "border-border bg-card hover:border-muted-foreground/40",
|
: "border-border bg-card hover:border-muted-foreground/40",
|
||||||
)}
|
)}
|
||||||
@@ -681,9 +888,9 @@ function ClientGeneralPage() {
|
|||||||
<input
|
<input
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="client-type"
|
name="security-profile"
|
||||||
checked={clientType === "pkce"}
|
checked={securityProfile === "pkce"}
|
||||||
onChange={() => setClientType("pkce")}
|
onChange={() => handleSecurityProfileChange("pkce")}
|
||||||
/>
|
/>
|
||||||
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
@@ -696,13 +903,231 @@ function ClientGeneralPage() {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute right-4 top-4 text-primary">
|
<span className="absolute right-4 top-4 text-primary">
|
||||||
{clientType === "pkce" ? "✓" : ""}
|
{securityProfile === "pkce" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{securityProfile === "pkce" && (
|
||||||
|
<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>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.security.trusted_rp_enable_help",
|
||||||
|
"Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="trusted-rp-toggle"
|
||||||
|
checked={headlessLoginEnabled}
|
||||||
|
onCheckedChange={handleHeadlessToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 4. Public Key Registration (Trusted RP) */}
|
||||||
|
{clientType === "pkce" && headlessLoginEnabled && (
|
||||||
|
<Card className="glass-panel border-primary/20">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.public_key.title",
|
||||||
|
"Public Key Registration",
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.general.public_key.subtitle",
|
||||||
|
"Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다.",
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className={cn("rounded-xl border p-4", publicKeyStatusTone)}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-bold text-foreground">
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.public_key.headless_toggle",
|
||||||
|
"Headless Login 허용 여부",
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.general.public_key.headless_help",
|
||||||
|
"애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="default"
|
||||||
|
className="bg-primary/20 text-primary border-primary/30"
|
||||||
|
>
|
||||||
|
{t("ui.common.enabled", "Enabled")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.public_key.request_object_alg",
|
||||||
|
"Request Object Signing Algorithm",
|
||||||
|
)}
|
||||||
|
<span className="text-destructive ml-1">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 rounded-xl border border-border bg-muted/5 p-4">
|
||||||
|
<div className="space-y-1 pb-2 border-b border-border/50">
|
||||||
|
<Label className="text-sm font-bold">
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.public_key.source",
|
||||||
|
"Public Key Source",
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.general.public_key.source_help",
|
||||||
|
"OIDC 검증을 위한 공개키 제공 방식을 선택합니다. (운영 환경에서는 JWKS URI 사용을 권장합니다)",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="jwksSource"
|
||||||
|
checked={jwksSource === "inline"}
|
||||||
|
onChange={() => setJwksSource("inline")}
|
||||||
|
className="accent-primary"
|
||||||
|
/>
|
||||||
|
<span>Inline Public Key</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="jwksSource"
|
||||||
|
checked={jwksSource === "uri"}
|
||||||
|
onChange={() => setJwksSource("uri")}
|
||||||
|
className="accent-primary"
|
||||||
|
/>
|
||||||
|
<span>JWKS URI</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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",
|
||||||
|
)}
|
||||||
|
<span className="text-destructive ml-1">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={jwksUri}
|
||||||
|
onChange={(e) => setJwksUri(e.target.value)}
|
||||||
|
placeholder={t(
|
||||||
|
"ui.dev.clients.general.public_key.jwks_uri_placeholder",
|
||||||
|
"https://rp.example.com/.well-known/jwks.json",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.general.public_key.jwks_uri_help",
|
||||||
|
"RP backend가 제공하는 공개키 endpoint URL을 입력하세요.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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 공개키",
|
||||||
|
)}
|
||||||
|
<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...' 형식의 공개키를 붙여넣으세요.",
|
||||||
|
)}
|
||||||
|
className="font-mono text-xs leading-tight"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.general.public_key.jwks_inline_help",
|
||||||
|
"OIDC 표준인 JWKS(JSON) 형식을 권장하지만, SSH-RSA 공개키를 입력하면 자동으로 변환하여 저장합니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasValidationErrors && (
|
||||||
|
<div className="rounded-xl border border-destructive/40 bg-destructive/5 p-4 animate-in fade-in">
|
||||||
|
<p className="text-sm font-semibold text-destructive flex items-center gap-2">
|
||||||
|
<span>⚠️</span>
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.public_key.validation_title",
|
||||||
|
"저장 전 확인 필요",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 list-disc space-y-1 pl-6 text-sm text-destructive">
|
||||||
|
{validationErrors.map((errorMessage) => (
|
||||||
|
<li key={errorMessage}>{errorMessage}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-border pt-4">
|
<div className="flex items-center justify-between border-t border-border pt-4">
|
||||||
<div>
|
<div>
|
||||||
{!isCreate && (
|
{!isCreate && (
|
||||||
@@ -729,7 +1154,8 @@ function ClientGeneralPage() {
|
|||||||
mutation.isPending ||
|
mutation.isPending ||
|
||||||
isLoading ||
|
isLoading ||
|
||||||
name.trim() === "" ||
|
name.trim() === "" ||
|
||||||
(isCreate && redirectUris.trim() === "")
|
(isCreate && redirectUris.trim() === "") ||
|
||||||
|
hasValidationErrors
|
||||||
}
|
}
|
||||||
className="shadow-lg shadow-primary/20"
|
className="shadow-lg shadow-primary/20"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -122,10 +122,11 @@ function formatTemplate(
|
|||||||
template: string,
|
template: string,
|
||||||
vars?: Record<string, string | number>,
|
vars?: Record<string, string | number>,
|
||||||
): string {
|
): string {
|
||||||
|
const normalizedTemplate = template.replace(/\\n/g, "\n");
|
||||||
if (!vars) {
|
if (!vars) {
|
||||||
return template;
|
return normalizedTemplate;
|
||||||
}
|
}
|
||||||
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
|
return normalizedTemplate.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
|
||||||
const value = vars[key];
|
const value = vars[key];
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return match;
|
return match;
|
||||||
|
|||||||
139
devfront/src/lib/keyUtils.ts
Normal file
139
devfront/src/lib/keyUtils.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Key Utilities for converting various public key formats (PEM, OpenSSH) to JWKS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface JWK {
|
||||||
|
kty: string;
|
||||||
|
n: string;
|
||||||
|
e: string;
|
||||||
|
kid?: string;
|
||||||
|
use?: string;
|
||||||
|
alg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Base64 string to a URL-safe Base64 string (RFC 7515).
|
||||||
|
*/
|
||||||
|
function toBase64Url(base64: string): string {
|
||||||
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a hex string to a URL-safe Base64 string.
|
||||||
|
*/
|
||||||
|
function hexToBase64Url(hex: string): string {
|
||||||
|
const binary = hex
|
||||||
|
.match(/.{1,2}/g)
|
||||||
|
?.map((byte) => String.fromCharCode(Number.parseInt(byte, 16)))
|
||||||
|
.join("");
|
||||||
|
if (!binary) return "";
|
||||||
|
return toBase64Url(btoa(binary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts RSA Modulus (n) and Exponent (e) from a SubjectPublicKeyInfo (PEM).
|
||||||
|
* This is a simplified parser for common RSA keys.
|
||||||
|
*/
|
||||||
|
export function parsePemToJwk(pem: string): JWK | null {
|
||||||
|
try {
|
||||||
|
// Remove headers, footers and whitespace
|
||||||
|
const base64 = pem
|
||||||
|
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
||||||
|
.replace(/-----END PUBLIC KEY-----/, "")
|
||||||
|
.replace(/\s/g, "");
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an OpenSSH Public Key (ssh-rsa AAAA...) into a JWK.
|
||||||
|
*/
|
||||||
|
export function parseSshRsaToJwk(sshKey: string): JWK | null {
|
||||||
|
try {
|
||||||
|
const parts = sshKey.trim().split(" ");
|
||||||
|
if (parts.length < 2 || parts[0] !== "ssh-rsa") return null;
|
||||||
|
|
||||||
|
const keyData = atob(parts[1]);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const readBlob = () => {
|
||||||
|
const len =
|
||||||
|
(keyData.charCodeAt(offset) << 24) |
|
||||||
|
(keyData.charCodeAt(offset + 1) << 16) |
|
||||||
|
(keyData.charCodeAt(offset + 2) << 8) |
|
||||||
|
keyData.charCodeAt(offset + 3);
|
||||||
|
offset += 4;
|
||||||
|
const blob = keyData.slice(offset, offset + len);
|
||||||
|
offset += len;
|
||||||
|
return blob;
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = readBlob(); // "ssh-rsa"
|
||||||
|
if (type !== "ssh-rsa") return null;
|
||||||
|
|
||||||
|
const eBlob = readBlob();
|
||||||
|
const nBlob = readBlob();
|
||||||
|
|
||||||
|
const toB64Url = (blob: string) => toBase64Url(btoa(blob));
|
||||||
|
|
||||||
|
return {
|
||||||
|
kty: "RSA",
|
||||||
|
n: semanticsBase64Url(nBlob),
|
||||||
|
e: semanticsBase64Url(eBlob),
|
||||||
|
alg: "RS256",
|
||||||
|
use: "sig",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse SSH key", e);
|
||||||
|
return 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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to auto-detect and convert input to JWKS JSON string.
|
||||||
|
* Returns the original string if it's already JSON or conversion fails.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try SSH RSA
|
||||||
|
if (trimmed.startsWith("ssh-rsa")) {
|
||||||
|
const jwk = parseSshRsaToJwk(trimmed);
|
||||||
|
if (jwk) {
|
||||||
|
return JSON.stringify({ keys: [jwk] }, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
@@ -390,6 +390,30 @@ subtitle = "Define the permission scopes this application can request."
|
|||||||
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
|
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
|
||||||
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
|
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
|
||||||
subtitle = "Select application type. Security level determines authentication method."
|
subtitle = "Select application type. Security level determines authentication method."
|
||||||
|
trusted_help = "Operate as a trusted RP using private_key_jwt and public key registration. Headless login is only available for this profile."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key]
|
||||||
|
auth_method_client_secret_basic_help = "Standard authentication method for server-side applications."
|
||||||
|
auth_method_none_help = "Use this for PKCE-based public clients."
|
||||||
|
auth_method_private_key_jwt_help = "Signed key-based client authentication recommended for trusted RP bootstrap and JAR verification."
|
||||||
|
guide_example = "Recommended example: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
guide_intro = "A JWKS URI is not created by Baron. It is the URL where the RP backend exposes its public key."
|
||||||
|
guide_step_1 = "Generate a key pair on the RP server and keep the private key only in the RP backend."
|
||||||
|
guide_step_2 = "Expose the public key from the RP backend through a JWKS (JSON Web Key Set) endpoint."
|
||||||
|
guide_step_3 = "Enter a URL such as https://rp.example.com/.well-known/jwks.json in DevFront."
|
||||||
|
headless_help = "You can design your own login UI within the application. While the UI is yours, the actual identity verification and security checks are handled in the background via Baron's API."
|
||||||
|
jwks_inline_help = "Prefer the SSH-RSA public key format first. If you paste an 'ssh-rsa AAA...' key, Baron converts it to OIDC-standard JWKS (JSON) before saving."
|
||||||
|
jwks_uri_help = "Enter the public key endpoint URL exposed by the RP backend. Example: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg_help = "Specify the JAR (Request Object) signing algorithm used for headless login."
|
||||||
|
source_help = "Register the JWKS URI served by the RP so Baron can verify the public key."
|
||||||
|
subtitle = "Manage the public key and headless login settings required for trusted RP evaluation."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key.validation]
|
||||||
|
headless_requires_alg = "Headless login requires a Request Object Signing Algorithm."
|
||||||
|
headless_requires_private_key_jwt = "Headless login requires token endpoint auth method to be private_key_jwt."
|
||||||
|
headless_requires_public_key = "Headless login requires a JWKS URI."
|
||||||
|
invalid_jwks_uri = "JWKS URI format is invalid."
|
||||||
|
private_key_jwt_requires_public_key = "Signed key-based authentication requires a JWKS URI."
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
@@ -1368,7 +1392,30 @@ delete = "Delete"
|
|||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
private = "Server Side App"
|
private = "Server Side App"
|
||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
|
trusted = "Trusted RP"
|
||||||
title = "Security Settings"
|
title = "Security Settings"
|
||||||
|
trusted_rp_enable = "Trusted RP (Custom Login UI)"
|
||||||
|
trusted_rp_enable_help = "Enable this if you want to implement your own login screen within the app instead of using the Baron SSO login page."
|
||||||
|
|
||||||
|
[ui.dev.clients.general.public_key]
|
||||||
|
auth_method = "Token Endpoint Auth Method"
|
||||||
|
auth_method_client_secret_basic = "client_secret_basic"
|
||||||
|
auth_method_none = "none"
|
||||||
|
auth_method_private_key_jwt = "Signed Key Authentication"
|
||||||
|
guide_toggle = "JWKS URI Setup Guide"
|
||||||
|
headless_disabled = "Headless Disabled"
|
||||||
|
headless_enabled = "Headless Enabled"
|
||||||
|
headless_toggle = "Headless Login"
|
||||||
|
jwks_inline = "SSH-RSA or JWKS Public Key"
|
||||||
|
jwks_inline_placeholder = "Paste an 'ssh-rsa AAA...' public key first. JWKS (JSON) is also accepted if needed."
|
||||||
|
jwks_uri = "JWKS URI"
|
||||||
|
jwks_uri_placeholder = "https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg = "Request Object Signing Algorithm"
|
||||||
|
request_object_alg_placeholder = "RS256"
|
||||||
|
source = "Public Key Source"
|
||||||
|
source_uri = "JWKS URI"
|
||||||
|
title = "Public Key Registration"
|
||||||
|
validation_title = "Check before saving"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
|
|||||||
@@ -390,6 +390,31 @@ subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
|||||||
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
|
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
|
||||||
private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
|
private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
|
||||||
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
|
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
|
||||||
|
trusted_help = "private_key_jwt와 공개키 등록을 사용해 trusted RP로 운영합니다.\nHeadless Login은 이 프로필에서만 사용할 수 있습니다."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key]
|
||||||
|
auth_method_client_secret_basic_help = "일반적인 서버 사이드 앱 인증 방식입니다."
|
||||||
|
auth_method_none_help = "PKCE 기반 public client에 사용하는 방식입니다."
|
||||||
|
auth_method_private_key_jwt_help = "Trusted RP bootstrap과 JAR 검증에 필요한 서명 키 기반 인증 방식입니다."
|
||||||
|
guide_example = "권장 예시: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
guide_intro = "JWKS URI는 Baron이 만드는 값이 아니라 RP backend가 공개키를 노출하는 URL입니다."
|
||||||
|
guide_step_1 = "RP 서버에서 key pair를 생성하고 private key는 RP backend에만 보관합니다."
|
||||||
|
guide_step_2 = "RP backend가 public key를 JWKS(JSON Web Key Set) 형태로 제공하는 endpoint를 준비합니다."
|
||||||
|
guide_step_3 = "예: https://rp.example.com/.well-known/jwks.json 같은 URL을 DevFront에 입력합니다."
|
||||||
|
headless_help = "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다."
|
||||||
|
jwks_inline_help = "SSH-RSA 공개키 형식을 우선 권장합니다. 'ssh-rsa AAA...' 형식으로 입력하면 Baron이 OIDC 표준인 JWKS(JSON)로 자동 변환하여 저장합니다."
|
||||||
|
jwks_uri_help = "RP backend가 제공하는 공개키 endpoint URL을 입력하세요. 예: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg_help = "Headless Login을 사용할 때 JAR(Request Object) 서명 알고리즘을 명시합니다."
|
||||||
|
source_help = "애플리케이션의 공개키(SSH-RSA)를 직접 등록하거나, 운영 환경이라면 JWKS URI를 통해 자동으로 검증할 수 있습니다."
|
||||||
|
subtitle = "Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key.validation]
|
||||||
|
headless_requires_alg = "Headless Login을 사용하려면 Request Object Signing Algorithm을 입력해야 합니다."
|
||||||
|
headless_requires_private_key_jwt = "Headless Login을 사용하려면 token endpoint auth method가 private_key_jwt여야 합니다."
|
||||||
|
headless_requires_public_key = "Headless Login을 사용하려면 JWKS URI가 필요합니다."
|
||||||
|
invalid_jwks_uri = "JWKS URI 형식이 올바르지 않습니다."
|
||||||
|
missing_jwks_inline = "공개키(SSH-RSA 또는 JWKS)를 입력해야 합니다."
|
||||||
|
private_key_jwt_requires_public_key = "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다."
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
@@ -1368,6 +1393,29 @@ delete = "삭제"
|
|||||||
private = "Server side App"
|
private = "Server side App"
|
||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
title = "보안 설정"
|
title = "보안 설정"
|
||||||
|
trusted_rp_enable = "Trusted RP (자체 로그인 UI 사용)"
|
||||||
|
trusted_rp_enable_help = "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다."
|
||||||
|
|
||||||
|
|
||||||
|
[ui.dev.clients.general.public_key]
|
||||||
|
auth_method = "Token Endpoint Auth Method"
|
||||||
|
auth_method_client_secret_basic = "client_secret_basic"
|
||||||
|
auth_method_none = "none"
|
||||||
|
auth_method_private_key_jwt = "서명 키 기반 인증"
|
||||||
|
guide_toggle = "JWKS URI 준비 가이드"
|
||||||
|
headless_disabled = "Headless Disabled"
|
||||||
|
headless_enabled = "Headless Enabled"
|
||||||
|
headless_toggle = "Headless Login"
|
||||||
|
jwks_inline = "SSH-RSA 또는 JWKS 공개키"
|
||||||
|
jwks_inline_placeholder = "'ssh-rsa AAA...' 형식의 공개키를 먼저 붙여넣으세요. 필요하면 JWKS (JSON)도 입력할 수 있습니다."
|
||||||
|
jwks_uri = "JWKS URI"
|
||||||
|
jwks_uri_placeholder = "https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg = "Request Object Signing Algorithm"
|
||||||
|
request_object_alg_placeholder = "RS256"
|
||||||
|
source = "Public Key Source"
|
||||||
|
source_uri = "JWKS URI"
|
||||||
|
title = "공개키 등록"
|
||||||
|
validation_title = "저장 전 확인 필요"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
|
|||||||
@@ -390,6 +390,30 @@ subtitle = ""
|
|||||||
private_help = ""
|
private_help = ""
|
||||||
pkce_help = ""
|
pkce_help = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
trusted_help = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key]
|
||||||
|
auth_method_client_secret_basic_help = ""
|
||||||
|
auth_method_none_help = ""
|
||||||
|
auth_method_private_key_jwt_help = ""
|
||||||
|
guide_example = ""
|
||||||
|
guide_intro = ""
|
||||||
|
guide_step_1 = ""
|
||||||
|
guide_step_2 = ""
|
||||||
|
guide_step_3 = ""
|
||||||
|
headless_help = ""
|
||||||
|
jwks_inline_help = ""
|
||||||
|
jwks_uri_help = ""
|
||||||
|
request_object_alg_help = ""
|
||||||
|
source_help = ""
|
||||||
|
subtitle = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key.validation]
|
||||||
|
headless_requires_alg = ""
|
||||||
|
headless_requires_private_key_jwt = ""
|
||||||
|
headless_requires_public_key = ""
|
||||||
|
invalid_jwks_uri = ""
|
||||||
|
private_key_jwt_requires_public_key = ""
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
docs_body = ""
|
docs_body = ""
|
||||||
@@ -1368,6 +1392,28 @@ delete = ""
|
|||||||
private = ""
|
private = ""
|
||||||
pkce = ""
|
pkce = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
trusted_rp_enable = ""
|
||||||
|
trusted_rp_enable_help = ""
|
||||||
|
|
||||||
|
[ui.dev.clients.general.public_key]
|
||||||
|
auth_method = ""
|
||||||
|
auth_method_client_secret_basic = ""
|
||||||
|
auth_method_none = ""
|
||||||
|
auth_method_private_key_jwt = ""
|
||||||
|
guide_toggle = ""
|
||||||
|
headless_disabled = ""
|
||||||
|
headless_enabled = ""
|
||||||
|
headless_toggle = ""
|
||||||
|
jwks_inline = ""
|
||||||
|
jwks_inline_placeholder = ""
|
||||||
|
jwks_uri = ""
|
||||||
|
jwks_uri_placeholder = ""
|
||||||
|
request_object_alg = ""
|
||||||
|
request_object_alg_placeholder = ""
|
||||||
|
source = ""
|
||||||
|
source_uri = ""
|
||||||
|
title = ""
|
||||||
|
validation_title = ""
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
docs_body = ""
|
docs_body = ""
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from "./helpers/devfront-fixtures";
|
} from "./helpers/devfront-fixtures";
|
||||||
|
|
||||||
const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
|
const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
|
||||||
|
const sshRsaPublicKey =
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAABwECAwQFBgc= test@example";
|
||||||
|
|
||||||
test.describe("DevFront clients lifecycle", () => {
|
test.describe("DevFront clients lifecycle", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -120,4 +122,77 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
page.getByRole("textbox", { name: /인증 콜백 URL|Callback/i }),
|
page.getByRole("textbox", { name: /인증 콜백 URL|Callback/i }),
|
||||||
).toHaveValue(/https:\/\/after\.example\.com\/callback/);
|
).toHaveValue(/https:\/\/after\.example\.com\/callback/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("pkce trusted rp with inline ssh-rsa key should persist mapped payload", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const state = {
|
||||||
|
clients: [
|
||||||
|
makeClient("client-trusted", { name: "Trusted App", type: "pkce" }),
|
||||||
|
],
|
||||||
|
consents: [] as Consent[],
|
||||||
|
auditLogsByCursor: undefined,
|
||||||
|
};
|
||||||
|
await installDevApiMock(page, state);
|
||||||
|
|
||||||
|
await page.goto("/clients/client-trusted/settings");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole("switch", {
|
||||||
|
name: /Trusted RP \(자체 로그인 UI 사용\)|Trusted RP \(Custom Login UI\)/i,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", {
|
||||||
|
name: /공개키 등록|Public Key Registration/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder(
|
||||||
|
/ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i,
|
||||||
|
)
|
||||||
|
.fill(sshRsaPublicKey);
|
||||||
|
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() => state.clients[0]?.tokenEndpointAuthMethod)
|
||||||
|
.toBe("private_key_jwt");
|
||||||
|
await expect
|
||||||
|
.poll(() => state.clients[0]?.metadata?.headless_login_enabled)
|
||||||
|
.toBe(true);
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
state.clients[0]?.jwks as {
|
||||||
|
keys?: Array<{ kty?: string; alg?: string }>;
|
||||||
|
}
|
||||||
|
)?.keys?.[0]?.kty,
|
||||||
|
)
|
||||||
|
.toBe("RSA");
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
state.clients[0]?.jwks as {
|
||||||
|
keys?: Array<{ kty?: string; alg?: string }>;
|
||||||
|
}
|
||||||
|
)?.keys?.[0]?.alg,
|
||||||
|
)
|
||||||
|
.toBe("RS256");
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", {
|
||||||
|
name: /공개키 등록|Public Key Registration/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByPlaceholder(
|
||||||
|
/ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i,
|
||||||
|
),
|
||||||
|
).toHaveValue(/"kty": "RSA"/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type Client = {
|
|||||||
scopes: string[];
|
scopes: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
|
tokenEndpointAuthMethod?: string;
|
||||||
|
jwksUri?: string;
|
||||||
|
jwks?: Record<string, unknown> | string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,6 +217,9 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
status?: ClientStatus;
|
status?: ClientStatus;
|
||||||
redirectUris?: string[];
|
redirectUris?: string[];
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
|
tokenEndpointAuthMethod?: string;
|
||||||
|
jwksUri?: string;
|
||||||
|
jwks?: Record<string, unknown> | string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}) || { name: "created app" };
|
}) || { name: "created app" };
|
||||||
|
|
||||||
@@ -223,6 +229,9 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
status: payload.status ?? "active",
|
status: payload.status ?? "active",
|
||||||
redirectUris: payload.redirectUris ?? [],
|
redirectUris: payload.redirectUris ?? [],
|
||||||
scopes: payload.scopes ?? ["openid"],
|
scopes: payload.scopes ?? ["openid"],
|
||||||
|
tokenEndpointAuthMethod: payload.tokenEndpointAuthMethod,
|
||||||
|
jwksUri: payload.jwksUri,
|
||||||
|
jwks: payload.jwks,
|
||||||
metadata: payload.metadata ?? {},
|
metadata: payload.metadata ?? {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,6 +303,9 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
type?: ClientType;
|
type?: ClientType;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
redirectUris?: string[];
|
redirectUris?: string[];
|
||||||
|
tokenEndpointAuthMethod?: string;
|
||||||
|
jwksUri?: string;
|
||||||
|
jwks?: Record<string, unknown> | string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}) || { name: "updated app" };
|
}) || { name: "updated app" };
|
||||||
const found = state.clients.find((client) => client.id === clientId);
|
const found = state.clients.find((client) => client.id === clientId);
|
||||||
@@ -302,6 +314,15 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
if (payload.type) found.type = payload.type;
|
if (payload.type) found.type = payload.type;
|
||||||
if (payload.scopes) found.scopes = payload.scopes;
|
if (payload.scopes) found.scopes = payload.scopes;
|
||||||
if (payload.redirectUris) found.redirectUris = payload.redirectUris;
|
if (payload.redirectUris) found.redirectUris = payload.redirectUris;
|
||||||
|
if (payload.tokenEndpointAuthMethod !== undefined) {
|
||||||
|
found.tokenEndpointAuthMethod = payload.tokenEndpointAuthMethod;
|
||||||
|
}
|
||||||
|
if (payload.jwksUri !== undefined) {
|
||||||
|
found.jwksUri = payload.jwksUri;
|
||||||
|
}
|
||||||
|
if (payload.jwks !== undefined) {
|
||||||
|
found.jwks = payload.jwks;
|
||||||
|
}
|
||||||
if (payload.metadata) found.metadata = payload.metadata;
|
if (payload.metadata) found.metadata = payload.metadata;
|
||||||
appendAuditLog("CLIENT_UPDATE", "UPDATE_CLIENT", clientId);
|
appendAuditLog("CLIENT_UPDATE", "UPDATE_CLIENT", clientId);
|
||||||
return json(route, {
|
return json(route, {
|
||||||
|
|||||||
@@ -234,6 +234,32 @@ limit_notice = "Showing members from the first 10 descendant organizations due t
|
|||||||
[msg.admin.tenants.registry]
|
[msg.admin.tenants.registry]
|
||||||
count = "{{count}} tenants loaded."
|
count = "{{count}} tenants loaded."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key]
|
||||||
|
auth_method_client_secret_basic_help = "Standard authentication method for server-side applications."
|
||||||
|
auth_method_none_help = "Use this for PKCE-based public clients."
|
||||||
|
auth_method_private_key_jwt_help = "Signed key-based client authentication recommended for trusted RP bootstrap and JAR verification."
|
||||||
|
guide_example = "Recommended example: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
guide_intro = "A JWKS URI is not created by Baron. It is the URL where the RP backend exposes its public key."
|
||||||
|
guide_step_1 = "Generate a key pair on the RP server and keep the private key only in the RP backend."
|
||||||
|
guide_step_2 = "Expose the public key from the RP backend through a JWKS (JSON Web Key Set) endpoint."
|
||||||
|
guide_step_3 = "Enter a URL such as https://rp.example.com/.well-known/jwks.json in DevFront."
|
||||||
|
headless_help = "You can design your own login UI within the application. While the UI is yours, the actual identity verification and security checks are handled in the background via Baron's API."
|
||||||
|
jwks_inline_help = "Prefer the SSH-RSA public key format first. If you paste an 'ssh-rsa AAA...' key, Baron converts it to OIDC-standard JWKS (JSON) before saving."
|
||||||
|
jwks_uri_help = "Enter the public key endpoint URL exposed by the RP backend. Example: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg_help = "Specify the JAR (Request Object) signing algorithm used for headless login."
|
||||||
|
source_help = "Register the JWKS URI served by the RP so Baron can verify the public key."
|
||||||
|
subtitle = "Manage the public key and headless login settings required for trusted RP evaluation."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key.validation]
|
||||||
|
headless_requires_alg = "Headless login requires a Request Object Signing Algorithm."
|
||||||
|
headless_requires_private_key_jwt = "Headless login requires token endpoint auth method to be private_key_jwt."
|
||||||
|
headless_requires_public_key = "Headless login requires a JWKS URI."
|
||||||
|
invalid_jwks_inline = "The input must be valid JSON (JWKS). For SSH-RSA input, it must start with 'ssh-rsa'."
|
||||||
|
invalid_jwks_uri = "JWKS URI format is invalid."
|
||||||
|
missing_jwks_inline = "Enter a public key in SSH-RSA or JWKS format."
|
||||||
|
missing_jwks_uri = "JWKS URI is required."
|
||||||
|
private_key_jwt_requires_public_key = "Signed key-based authentication requires a JWKS URI."
|
||||||
|
|
||||||
[msg.admin.tenants.schema]
|
[msg.admin.tenants.schema]
|
||||||
empty = "No custom fields defined. Click \"Add Field\" to begin."
|
empty = "No custom fields defined. Click \"Add Field\" to begin."
|
||||||
missing_id = "Tenant ID missing"
|
missing_id = "Tenant ID missing"
|
||||||
@@ -1215,6 +1241,7 @@ create = "Create"
|
|||||||
delete = "Delete"
|
delete = "Delete"
|
||||||
details = "Details"
|
details = "Details"
|
||||||
edit = "Edit"
|
edit = "Edit"
|
||||||
|
enabled = "Enabled"
|
||||||
export = "Export"
|
export = "Export"
|
||||||
fail = "Fail"
|
fail = "Fail"
|
||||||
go_home = "Go Home"
|
go_home = "Go Home"
|
||||||
@@ -1459,6 +1486,28 @@ delete = "Delete"
|
|||||||
private = "Server Side App"
|
private = "Server Side App"
|
||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
title = "Security Settings"
|
title = "Security Settings"
|
||||||
|
trusted_rp_enable = "Trusted RP (Custom Login UI)"
|
||||||
|
trusted_rp_enable_help = "Enable this if you want to implement your own login screen within the app instead of using the Baron SSO login page."
|
||||||
|
|
||||||
|
[ui.dev.clients.general.public_key]
|
||||||
|
auth_method = "Token Endpoint Auth Method"
|
||||||
|
auth_method_client_secret_basic = "client_secret_basic"
|
||||||
|
auth_method_none = "none"
|
||||||
|
auth_method_private_key_jwt = "Signed Key Authentication"
|
||||||
|
guide_toggle = "JWKS URI Setup Guide"
|
||||||
|
headless_disabled = "Headless Disabled"
|
||||||
|
headless_enabled = "Headless Enabled"
|
||||||
|
headless_toggle = "Headless Login"
|
||||||
|
jwks_inline = "SSH-RSA or JWKS Public Key"
|
||||||
|
jwks_inline_placeholder = "Paste an 'ssh-rsa AAA...' public key first. JWKS (JSON) is also accepted if needed."
|
||||||
|
jwks_uri = "JWKS URI"
|
||||||
|
jwks_uri_placeholder = "https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg = "Request Object Signing Algorithm"
|
||||||
|
request_object_alg_placeholder = "RS256"
|
||||||
|
source = "Public Key Source"
|
||||||
|
source_uri = "JWKS URI"
|
||||||
|
title = "Public Key Registration"
|
||||||
|
validation_title = "Check before saving"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ create = "생성"
|
|||||||
delete = "삭제"
|
delete = "삭제"
|
||||||
details = "상세정보"
|
details = "상세정보"
|
||||||
edit = "편집"
|
edit = "편집"
|
||||||
|
enabled = "사용"
|
||||||
export = "내보내기"
|
export = "내보내기"
|
||||||
fail = "실패"
|
fail = "실패"
|
||||||
go_home = "홈으로"
|
go_home = "홈으로"
|
||||||
@@ -229,6 +230,32 @@ showing = "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다."
|
|||||||
notice = "개발자 전용 콘솔입니다."
|
notice = "개발자 전용 콘솔입니다."
|
||||||
notice_detail = "연동 앱 등록 및 관리를 수행할 수 있습니다."
|
notice_detail = "연동 앱 등록 및 관리를 수행할 수 있습니다."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key]
|
||||||
|
auth_method_client_secret_basic_help = "일반적인 서버 사이드 앱 인증 방식입니다."
|
||||||
|
auth_method_none_help = "PKCE 기반 public client에 사용하는 방식입니다."
|
||||||
|
auth_method_private_key_jwt_help = "Trusted RP bootstrap과 JAR 검증에 필요한 서명 키 기반 인증 방식입니다."
|
||||||
|
guide_example = "권장 예시: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
guide_intro = "JWKS URI는 Baron이 만드는 값이 아니라 RP backend가 공개키를 노출하는 URL입니다."
|
||||||
|
guide_step_1 = "RP 서버에서 key pair를 생성하고 private key는 RP backend에만 보관합니다."
|
||||||
|
guide_step_2 = "RP backend가 public key를 JWKS(JSON Web Key Set) 형태로 제공하는 endpoint를 준비합니다."
|
||||||
|
guide_step_3 = "예: https://rp.example.com/.well-known/jwks.json 같은 URL을 DevFront에 입력합니다."
|
||||||
|
headless_help = "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다."
|
||||||
|
jwks_inline_help = "SSH-RSA 공개키 형식을 우선 권장합니다. 'ssh-rsa AAA...' 형식으로 입력하면 Baron이 OIDC 표준인 JWKS(JSON)로 자동 변환하여 저장합니다."
|
||||||
|
jwks_uri_help = "RP backend가 제공하는 공개키 endpoint URL을 입력하세요. 예: https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg_help = "Headless Login을 사용할 때 JAR(Request Object) 서명 알고리즘을 명시합니다."
|
||||||
|
source_help = "애플리케이션의 공개키(SSH-RSA)를 직접 등록하거나, 운영 환경이라면 JWKS URI를 통해 자동으로 검증할 수 있습니다."
|
||||||
|
subtitle = "Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다."
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key.validation]
|
||||||
|
headless_requires_alg = "Headless Login을 사용하려면 Request Object Signing Algorithm을 입력해야 합니다."
|
||||||
|
headless_requires_private_key_jwt = "Headless Login을 사용하려면 token endpoint auth method가 private_key_jwt여야 합니다."
|
||||||
|
headless_requires_public_key = "Headless Login을 사용하려면 JWKS URI가 필요합니다."
|
||||||
|
invalid_jwks_inline = "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다."
|
||||||
|
invalid_jwks_uri = "JWKS URI 형식이 올바르지 않습니다."
|
||||||
|
missing_jwks_inline = "공개키(SSH-RSA 또는 JWKS)를 입력해야 합니다."
|
||||||
|
missing_jwks_uri = "JWKS URI를 입력해야 합니다."
|
||||||
|
private_key_jwt_requires_public_key = "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다."
|
||||||
|
|
||||||
[msg.userfront.audit]
|
[msg.userfront.audit]
|
||||||
date = "접속일자: {{value}}"
|
date = "접속일자: {{value}}"
|
||||||
device = "접속환경: {{value}}"
|
device = "접속환경: {{value}}"
|
||||||
@@ -1712,6 +1739,28 @@ title = "스코프"
|
|||||||
private = "Server side App"
|
private = "Server side App"
|
||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
title = "보안 설정"
|
title = "보안 설정"
|
||||||
|
trusted_rp_enable = "Trusted RP (자체 로그인 UI 사용)"
|
||||||
|
trusted_rp_enable_help = "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다."
|
||||||
|
|
||||||
|
[ui.dev.clients.general.public_key]
|
||||||
|
auth_method = "Token Endpoint Auth Method"
|
||||||
|
auth_method_client_secret_basic = "client_secret_basic"
|
||||||
|
auth_method_none = "none"
|
||||||
|
auth_method_private_key_jwt = "서명 키 기반 인증"
|
||||||
|
guide_toggle = "JWKS URI 준비 가이드"
|
||||||
|
headless_disabled = "Headless Disabled"
|
||||||
|
headless_enabled = "Headless Enabled"
|
||||||
|
headless_toggle = "Headless Login"
|
||||||
|
jwks_inline = "SSH-RSA 또는 JWKS 공개키"
|
||||||
|
jwks_inline_placeholder = "'ssh-rsa AAA...' 형식의 공개키를 먼저 붙여넣으세요. 필요하면 JWKS (JSON)도 입력할 수 있습니다."
|
||||||
|
jwks_uri = "JWKS URI"
|
||||||
|
jwks_uri_placeholder = "https://rp.example.com/.well-known/jwks.json"
|
||||||
|
request_object_alg = "Request Object Signing Algorithm"
|
||||||
|
request_object_alg_placeholder = "RS256"
|
||||||
|
source = "Public Key Source"
|
||||||
|
source_uri = "JWKS URI"
|
||||||
|
title = "공개키 등록"
|
||||||
|
validation_title = "저장 전 확인 필요"
|
||||||
|
|
||||||
[ui.dev.dashboard.ops.card]
|
[ui.dev.dashboard.ops.card]
|
||||||
consent_revoked = "Consent 회수 건수"
|
consent_revoked = "Consent 회수 건수"
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ create = ""
|
|||||||
delete = ""
|
delete = ""
|
||||||
details = ""
|
details = ""
|
||||||
edit = ""
|
edit = ""
|
||||||
|
enabled = ""
|
||||||
export = ""
|
export = ""
|
||||||
fail = ""
|
fail = ""
|
||||||
go_home = ""
|
go_home = ""
|
||||||
@@ -229,6 +230,32 @@ delete_confirm = ""
|
|||||||
notice = ""
|
notice = ""
|
||||||
notice_detail = ""
|
notice_detail = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key]
|
||||||
|
auth_method_client_secret_basic_help = ""
|
||||||
|
auth_method_none_help = ""
|
||||||
|
auth_method_private_key_jwt_help = ""
|
||||||
|
guide_example = ""
|
||||||
|
guide_intro = ""
|
||||||
|
guide_step_1 = ""
|
||||||
|
guide_step_2 = ""
|
||||||
|
guide_step_3 = ""
|
||||||
|
headless_help = ""
|
||||||
|
jwks_inline_help = ""
|
||||||
|
jwks_uri_help = ""
|
||||||
|
request_object_alg_help = ""
|
||||||
|
source_help = ""
|
||||||
|
subtitle = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.general.public_key.validation]
|
||||||
|
headless_requires_alg = ""
|
||||||
|
headless_requires_private_key_jwt = ""
|
||||||
|
headless_requires_public_key = ""
|
||||||
|
invalid_jwks_inline = ""
|
||||||
|
invalid_jwks_uri = ""
|
||||||
|
missing_jwks_inline = ""
|
||||||
|
missing_jwks_uri = ""
|
||||||
|
private_key_jwt_requires_public_key = ""
|
||||||
|
|
||||||
[msg.userfront.audit]
|
[msg.userfront.audit]
|
||||||
date = ""
|
date = ""
|
||||||
device = ""
|
device = ""
|
||||||
@@ -1706,6 +1733,28 @@ title = ""
|
|||||||
private = ""
|
private = ""
|
||||||
pkce = ""
|
pkce = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
trusted_rp_enable = ""
|
||||||
|
trusted_rp_enable_help = ""
|
||||||
|
|
||||||
|
[ui.dev.clients.general.public_key]
|
||||||
|
auth_method = ""
|
||||||
|
auth_method_client_secret_basic = ""
|
||||||
|
auth_method_none = ""
|
||||||
|
auth_method_private_key_jwt = ""
|
||||||
|
guide_toggle = ""
|
||||||
|
headless_disabled = ""
|
||||||
|
headless_enabled = ""
|
||||||
|
headless_toggle = ""
|
||||||
|
jwks_inline = ""
|
||||||
|
jwks_inline_placeholder = ""
|
||||||
|
jwks_uri = ""
|
||||||
|
jwks_uri_placeholder = ""
|
||||||
|
request_object_alg = ""
|
||||||
|
request_object_alg_placeholder = ""
|
||||||
|
source = ""
|
||||||
|
source_uri = ""
|
||||||
|
title = ""
|
||||||
|
validation_title = ""
|
||||||
|
|
||||||
[ui.dev.dashboard.ops.card]
|
[ui.dev.dashboard.ops.card]
|
||||||
consent_revoked = ""
|
consent_revoked = ""
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ create = "Create"
|
|||||||
delete = "Delete"
|
delete = "Delete"
|
||||||
details = "Details"
|
details = "Details"
|
||||||
edit = "Edit"
|
edit = "Edit"
|
||||||
|
enabled = "Enabled"
|
||||||
export = "Export"
|
export = "Export"
|
||||||
fail = "Fail"
|
fail = "Fail"
|
||||||
go_home = "Go Home"
|
go_home = "Go Home"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ create = "생성"
|
|||||||
delete = "삭제"
|
delete = "삭제"
|
||||||
details = "상세정보"
|
details = "상세정보"
|
||||||
edit = "편집"
|
edit = "편집"
|
||||||
|
enabled = "사용"
|
||||||
export = "내보내기"
|
export = "내보내기"
|
||||||
fail = "실패"
|
fail = "실패"
|
||||||
go_home = "홈으로"
|
go_home = "홈으로"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ create = ""
|
|||||||
delete = ""
|
delete = ""
|
||||||
details = ""
|
details = ""
|
||||||
edit = ""
|
edit = ""
|
||||||
|
enabled = ""
|
||||||
export = ""
|
export = ""
|
||||||
fail = ""
|
fail = ""
|
||||||
go_home = ""
|
go_home = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user