forked from baron/baron-sso
Merge pull request 'feature/df-healess-alter' (#695) from feature/df-healess-alter into dev
Reviewed-on: baron/baron-sso#695
This commit is contained in:
@@ -1648,17 +1648,6 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private")
|
return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private")
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Security] Check permission for private clients
|
|
||||||
if clientType == "private" {
|
|
||||||
isAppManager, err := h.checkAppManagerPermission(c)
|
|
||||||
if err != nil {
|
|
||||||
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
|
|
||||||
}
|
|
||||||
if !isAppManager && !h.canManageTenantClientsByPermit(c, profile, tenantID) {
|
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions to create private client")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
|
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
|
||||||
if status != "active" && status != "inactive" {
|
if status != "active" && status != "inactive" {
|
||||||
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
|
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
|
||||||
@@ -1700,6 +1689,18 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
clientType = normalizeClientTypeForHeadless(clientType, metadata)
|
||||||
|
|
||||||
|
// [Security] Check permission for private clients
|
||||||
|
if clientType == "private" {
|
||||||
|
isAppManager, err := h.checkAppManagerPermission(c)
|
||||||
|
if err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
|
||||||
|
}
|
||||||
|
if !isAppManager && !h.canManageTenantClientsByPermit(c, profile, tenantID) {
|
||||||
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions to create private client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
||||||
if tokenAuthMethod == "" {
|
if tokenAuthMethod == "" {
|
||||||
@@ -1709,11 +1710,10 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
tokenAuthMethod = "client_secret_basic"
|
tokenAuthMethod = "client_secret_basic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := validateHeadlessClientInput(clientType, valueOr(req.JwksUri, ""), req.Jwks, metadata); err != nil {
|
if err := validateHeadlessClientInput(valueOr(req.JwksUri, ""), req.Jwks, metadata); err != nil {
|
||||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
tokenAuthMethod, jwksURI, jwks, metadata := normalizeHeadlessClientConfig(
|
tokenAuthMethod, jwksURI, jwks, metadata := normalizeHeadlessClientConfig(
|
||||||
clientType,
|
|
||||||
tokenAuthMethod,
|
tokenAuthMethod,
|
||||||
valueOr(req.JwksUri, ""),
|
valueOr(req.JwksUri, ""),
|
||||||
req.Jwks,
|
req.Jwks,
|
||||||
@@ -1900,21 +1900,21 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
if clientType != "" {
|
if clientType != "" {
|
||||||
resolvedClientType = clientType
|
resolvedClientType = clientType
|
||||||
}
|
}
|
||||||
|
resolvedClientType = normalizeClientTypeForHeadless(resolvedClientType, metadata)
|
||||||
resolvedTokenAuthMethod := resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod)
|
resolvedTokenAuthMethod := resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod)
|
||||||
resolvedJWKSURI := valueOr(req.JwksUri, current.JWKSUri)
|
resolvedJWKSURI := valueOr(req.JwksUri, current.JWKSUri)
|
||||||
resolvedJWKS := req.Jwks
|
resolvedJWKS := req.Jwks
|
||||||
if req.Jwks == nil {
|
if req.Jwks == nil {
|
||||||
if resolvedClientType == "pkce" && readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
if readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
||||||
resolvedJWKS = nil
|
resolvedJWKS = nil
|
||||||
} else {
|
} else {
|
||||||
resolvedJWKS = current.JWKS
|
resolvedJWKS = current.JWKS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := validateHeadlessClientInput(resolvedClientType, resolvedJWKSURI, resolvedJWKS, metadata); err != nil {
|
if err := validateHeadlessClientInput(resolvedJWKSURI, resolvedJWKS, metadata); err != nil {
|
||||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
resolvedTokenAuthMethod, resolvedJWKSURI, resolvedJWKS, metadata = normalizeHeadlessClientConfig(
|
resolvedTokenAuthMethod, resolvedJWKSURI, resolvedJWKS, metadata = normalizeHeadlessClientConfig(
|
||||||
resolvedClientType,
|
|
||||||
resolvedTokenAuthMethod,
|
resolvedTokenAuthMethod,
|
||||||
resolvedJWKSURI,
|
resolvedJWKSURI,
|
||||||
resolvedJWKS,
|
resolvedJWKS,
|
||||||
@@ -2633,12 +2633,10 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientType := "private"
|
clientType := "private"
|
||||||
if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
|
if client.IsHeadlessLoginEnabled() {
|
||||||
|
clientType = "private"
|
||||||
|
} else if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
|
||||||
clientType = "pkce"
|
clientType = "pkce"
|
||||||
} else if strings.EqualFold(client.TokenEndpointAuthMethod, "private_key_jwt") && client.Metadata != nil {
|
|
||||||
if val, ok := client.Metadata["headless_login_enabled"].(bool); ok && val {
|
|
||||||
clientType = "pkce"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name := strings.TrimSpace(client.ClientName)
|
name := strings.TrimSpace(client.ClientName)
|
||||||
@@ -2786,7 +2784,6 @@ func normalizeClientAutoLoginMetadata(metadata map[string]interface{}) (map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizeHeadlessClientConfig(
|
func normalizeHeadlessClientConfig(
|
||||||
clientType string,
|
|
||||||
tokenAuthMethod string,
|
tokenAuthMethod string,
|
||||||
jwksURI string,
|
jwksURI string,
|
||||||
jwks interface{},
|
jwks interface{},
|
||||||
@@ -2798,12 +2795,12 @@ func normalizeHeadlessClientConfig(
|
|||||||
delete(metadata, domain.MetadataRequestObjectSigningAlg)
|
delete(metadata, domain.MetadataRequestObjectSigningAlg)
|
||||||
|
|
||||||
headlessEnabled := readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled)
|
headlessEnabled := readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled)
|
||||||
if clientType == "pkce" && headlessEnabled {
|
if headlessEnabled {
|
||||||
headlessTokenAuthMethod := readMetadataStringValue(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod)
|
headlessTokenAuthMethod := readMetadataStringValue(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod)
|
||||||
if headlessTokenAuthMethod == "" && !strings.EqualFold(strings.TrimSpace(tokenAuthMethod), "none") {
|
if headlessTokenAuthMethod == "" && strings.EqualFold(strings.TrimSpace(tokenAuthMethod), "private_key_jwt") {
|
||||||
headlessTokenAuthMethod = strings.TrimSpace(tokenAuthMethod)
|
headlessTokenAuthMethod = strings.TrimSpace(tokenAuthMethod)
|
||||||
}
|
}
|
||||||
if headlessTokenAuthMethod == "" {
|
if headlessTokenAuthMethod == "" || strings.EqualFold(headlessTokenAuthMethod, "none") {
|
||||||
headlessTokenAuthMethod = "private_key_jwt"
|
headlessTokenAuthMethod = "private_key_jwt"
|
||||||
}
|
}
|
||||||
metadata[domain.MetadataHeadlessTokenEndpointAuthMethod] = headlessTokenAuthMethod
|
metadata[domain.MetadataHeadlessTokenEndpointAuthMethod] = headlessTokenAuthMethod
|
||||||
@@ -2820,7 +2817,7 @@ func normalizeHeadlessClientConfig(
|
|||||||
|
|
||||||
delete(metadata, domain.MetadataHeadlessJWKS)
|
delete(metadata, domain.MetadataHeadlessJWKS)
|
||||||
|
|
||||||
return "none", "", nil, metadata
|
return headlessTokenAuthMethod, headlessJWKSURI, nil, metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod)
|
delete(metadata, domain.MetadataHeadlessTokenEndpointAuthMethod)
|
||||||
@@ -2829,8 +2826,8 @@ func normalizeHeadlessClientConfig(
|
|||||||
return tokenAuthMethod, jwksURI, jwks, metadata
|
return tokenAuthMethod, jwksURI, jwks, metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateHeadlessClientInput(clientType string, jwksURI string, jwks interface{}, metadata map[string]interface{}) error {
|
func validateHeadlessClientInput(jwksURI string, jwks interface{}, metadata map[string]interface{}) error {
|
||||||
if clientType != "pkce" || !readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
if !readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2848,6 +2845,13 @@ func validateHeadlessClientInput(clientType string, jwksURI string, jwks interfa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeClientTypeForHeadless(clientType string, metadata map[string]interface{}) string {
|
||||||
|
if readMetadataBoolValue(metadata, domain.MetadataHeadlessLoginEnabled) {
|
||||||
|
return "private"
|
||||||
|
}
|
||||||
|
return clientType
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeIDTokenClaimsMetadata(metadata map[string]interface{}) (map[string]interface{}, error) {
|
func normalizeIDTokenClaimsMetadata(metadata map[string]interface{}) (map[string]interface{}, error) {
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -1467,7 +1467,7 @@ func TestCreateClient_HeadlessLoginPayloadMapping(t *testing.T) {
|
|||||||
|
|
||||||
body, _ := json.Marshal(map[string]any{
|
body, _ := json.Marshal(map[string]any{
|
||||||
"name": "Headless Login App",
|
"name": "Headless Login App",
|
||||||
"type": "pkce",
|
"type": "private",
|
||||||
"redirectUris": []string{"https://rp.example.com/callback"},
|
"redirectUris": []string{"https://rp.example.com/callback"},
|
||||||
"scopes": []string{"openid", "profile"},
|
"scopes": []string{"openid", "profile"},
|
||||||
"tokenEndpointAuthMethod": "private_key_jwt",
|
"tokenEndpointAuthMethod": "private_key_jwt",
|
||||||
@@ -1482,7 +1482,8 @@ func TestCreateClient_HeadlessLoginPayloadMapping(t *testing.T) {
|
|||||||
|
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
assert.Equal(t, "none", captured.TokenEndpointAuthMethod)
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
|
||||||
assert.Nil(t, captured.JWKS)
|
assert.Nil(t, captured.JWKS)
|
||||||
assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
|
assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
|
||||||
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
|
||||||
@@ -1633,6 +1634,87 @@ func TestCreateClient_AllowsExplicitSkipConsentFalse(t *testing.T) {
|
|||||||
assert.False(t, *captured.SkipConsent)
|
assert.False(t, *captured.SkipConsent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateClient_LegacyPKCEHeadlessInputIsNormalizedToPrivate(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_uri": captured.JWKSUri,
|
||||||
|
"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": "Legacy Headless Login App",
|
||||||
|
"type": "pkce",
|
||||||
|
"redirectUris": []string{"https://rp.example.com/callback"},
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
"headless_jwks_uri": "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
|
||||||
|
assert.Nil(t, captured.JWKS)
|
||||||
|
assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
|
||||||
|
assert.True(t, captured.IsHeadlessLoginEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapClientSummary_ClassifiesHeadlessLoginAsPrivate(t *testing.T) {
|
||||||
|
h := &DevHandler{}
|
||||||
|
|
||||||
|
summary := h.mapClientSummary(domain.HydraClient{
|
||||||
|
ClientID: "client-headless-login",
|
||||||
|
ClientName: "Headless Login App",
|
||||||
|
TokenEndpointAuthMethod: "none",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"status": "active",
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
"headless_token_endpoint_auth_method": "private_key_jwt",
|
||||||
|
"headless_jwks_uri": "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "private", summary.Type)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateClient_HeadlessLoginRejectsInlineJWKS(t *testing.T) {
|
func TestCreateClient_HeadlessLoginRejectsInlineJWKS(t *testing.T) {
|
||||||
var hydraCalled bool
|
var hydraCalled bool
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
@@ -1895,7 +1977,7 @@ func TestUpdateClient_HeadlessLoginPayloadMapping(t *testing.T) {
|
|||||||
|
|
||||||
body, _ := json.Marshal(map[string]any{
|
body, _ := json.Marshal(map[string]any{
|
||||||
"name": "Headless Login After",
|
"name": "Headless Login After",
|
||||||
"type": "pkce",
|
"type": "private",
|
||||||
"tokenEndpointAuthMethod": "private_key_jwt",
|
"tokenEndpointAuthMethod": "private_key_jwt",
|
||||||
"jwksUri": "https://rp.example.com/.well-known/jwks.json",
|
"jwksUri": "https://rp.example.com/.well-known/jwks.json",
|
||||||
"metadata": map[string]any{
|
"metadata": map[string]any{
|
||||||
@@ -1908,8 +1990,8 @@ func TestUpdateClient_HeadlessLoginPayloadMapping(t *testing.T) {
|
|||||||
|
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
assert.Equal(t, "none", captured.TokenEndpointAuthMethod)
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
assert.Equal(t, "", captured.JWKSUri)
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
|
||||||
assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
|
assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
|
||||||
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
|
||||||
_, hasInlineJWKS := captured.Metadata["headless_jwks"]
|
_, hasInlineJWKS := captured.Metadata["headless_jwks"]
|
||||||
@@ -2071,7 +2153,8 @@ func TestUpdateClient_HeadlessLoginIgnoresExistingTopLevelJWKS(t *testing.T) {
|
|||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
assert.Nil(t, captured.JWKS)
|
assert.Nil(t, captured.JWKS)
|
||||||
assert.Equal(t, "", captured.JWKSUri)
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
|
||||||
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
|
||||||
_, hasRequestObjectAlg := captured.Metadata["request_object_signing_alg"]
|
_, hasRequestObjectAlg := captured.Metadata["request_object_signing_alg"]
|
||||||
assert.False(t, hasRequestObjectAlg)
|
assert.False(t, hasRequestObjectAlg)
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ function ClientDetailsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const client = data?.client;
|
const client = data?.client;
|
||||||
|
const isHeadlessLogin = client?.metadata?.headless_login_enabled === true;
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -213,16 +214,21 @@ function ClientDetailsPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasClientSecret = client.type === "private";
|
const hasClientSecret = client.type === "private" && !isHeadlessLogin;
|
||||||
const secretPlaceholder = "SECRET_NOT_AVAILABLE";
|
const secretPlaceholder = "SECRET_NOT_AVAILABLE";
|
||||||
const clientSecret = hasClientSecret
|
const clientSecret = hasClientSecret
|
||||||
? client?.clientSecret || secretPlaceholder
|
? client?.clientSecret || secretPlaceholder
|
||||||
: t("ui.common.na", "N/A");
|
: t("ui.common.na", "N/A");
|
||||||
const displaySecret = !hasClientSecret
|
const displaySecret = !hasClientSecret
|
||||||
? t(
|
? isHeadlessLogin
|
||||||
"msg.dev.clients.details.secret_not_applicable",
|
? t(
|
||||||
"PKCE 앱에는 Client Secret이 없습니다.",
|
"msg.dev.clients.details.secret_not_applicable_headless",
|
||||||
)
|
"이 앱은 Headless Login용 signed key 인증을 사용하므로 Client Secret을 사용하지 않습니다.",
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"msg.dev.clients.details.secret_not_applicable",
|
||||||
|
"PKCE 앱에는 Client Secret이 없습니다.",
|
||||||
|
)
|
||||||
: clientSecret === secretPlaceholder
|
: clientSecret === secretPlaceholder
|
||||||
? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE")
|
? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE")
|
||||||
: clientSecret;
|
: clientSecret;
|
||||||
@@ -394,10 +400,15 @@ function ClientDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
{!hasClientSecret ? (
|
{!hasClientSecret ? (
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
{t(
|
{isHeadlessLogin
|
||||||
"msg.dev.clients.details.secret_not_applicable",
|
? t(
|
||||||
"PKCE 앱에는 Client Secret이 없습니다.",
|
"msg.dev.clients.details.secret_not_applicable_headless",
|
||||||
)}
|
"이 앱은 Headless Login용 signed key 인증을 사용하므로 Client Secret을 사용하지 않습니다.",
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"msg.dev.clients.details.secret_not_applicable",
|
||||||
|
"PKCE 앱에는 Client Secret이 없습니다.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -391,12 +391,13 @@ function ClientGeneralPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const { client } = data;
|
const { client } = data;
|
||||||
|
const metadata = client.metadata ?? {};
|
||||||
|
const headlessEnabled = !!metadata.headless_login_enabled;
|
||||||
|
|
||||||
setName(client.name || client.id);
|
setName(client.name || client.id);
|
||||||
setClientType(client.type);
|
setClientType(headlessEnabled ? "private" : client.type);
|
||||||
setStatus(client.status);
|
setStatus(client.status);
|
||||||
setInitialStatus(client.status);
|
setInitialStatus(client.status);
|
||||||
|
|
||||||
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);
|
||||||
@@ -412,7 +413,6 @@ function ClientGeneralPage() {
|
|||||||
if (typeof metadata.auto_login_url === "string")
|
if (typeof metadata.auto_login_url === "string")
|
||||||
setAutoLoginUrl(metadata.auto_login_url);
|
setAutoLoginUrl(metadata.auto_login_url);
|
||||||
|
|
||||||
const headlessEnabled = !!metadata.headless_login_enabled;
|
|
||||||
setHeadlessLoginEnabled(headlessEnabled);
|
setHeadlessLoginEnabled(headlessEnabled);
|
||||||
const restrictedTenants = Array.isArray(metadata.allowed_tenants)
|
const restrictedTenants = Array.isArray(metadata.allowed_tenants)
|
||||||
? metadata.allowed_tenants
|
? metadata.allowed_tenants
|
||||||
@@ -532,18 +532,25 @@ function ClientGeneralPage() {
|
|||||||
const handleSecurityProfileChange = (profile: SecurityProfile) => {
|
const handleSecurityProfileChange = (profile: SecurityProfile) => {
|
||||||
setClientType(profile);
|
setClientType(profile);
|
||||||
if (profile === "pkce") {
|
if (profile === "pkce") {
|
||||||
setTokenEndpointAuthMethod(
|
setHeadlessLoginEnabled(false);
|
||||||
headlessLoginEnabled ? "private_key_jwt" : "none",
|
setTokenEndpointAuthMethod("none");
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setTokenEndpointAuthMethod("client_secret_basic");
|
setTokenEndpointAuthMethod(
|
||||||
|
headlessLoginEnabled ? "private_key_jwt" : "client_secret_basic",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHeadlessToggle = (enabled: boolean) => {
|
const handleHeadlessToggle = (enabled: boolean) => {
|
||||||
setHeadlessLoginEnabled(enabled);
|
setHeadlessLoginEnabled(enabled);
|
||||||
if (clientType === "pkce") {
|
if (enabled) {
|
||||||
setTokenEndpointAuthMethod(enabled ? "private_key_jwt" : "none");
|
setClientType("private");
|
||||||
|
setTokenEndpointAuthMethod("private_key_jwt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientType === "private") {
|
||||||
|
setTokenEndpointAuthMethod("client_secret_basic");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -974,18 +981,17 @@ function ClientGeneralPage() {
|
|||||||
.map((scope) => scope.name.trim())
|
.map((scope) => scope.name.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const effectiveTokenEndpointAuthMethod =
|
const persistedClientType = headlessLoginEnabled ? "private" : clientType;
|
||||||
clientType === "pkce" && headlessLoginEnabled
|
const effectiveTokenEndpointAuthMethod = tokenEndpointAuthMethod;
|
||||||
? "none"
|
|
||||||
: tokenEndpointAuthMethod;
|
|
||||||
|
|
||||||
const payload: ClientUpsertRequest = {
|
const payload: ClientUpsertRequest = {
|
||||||
name,
|
name,
|
||||||
type: clientType,
|
type: persistedClientType,
|
||||||
scopes: scopeNames,
|
scopes: scopeNames,
|
||||||
tokenEndpointAuthMethod: effectiveTokenEndpointAuthMethod,
|
tokenEndpointAuthMethod: effectiveTokenEndpointAuthMethod,
|
||||||
jwksUri:
|
jwksUri:
|
||||||
effectiveTokenEndpointAuthMethod === "private_key_jwt" &&
|
(headlessLoginEnabled ||
|
||||||
|
effectiveTokenEndpointAuthMethod === "private_key_jwt") &&
|
||||||
trimmedJwksUri
|
trimmedJwksUri
|
||||||
? trimmedJwksUri
|
? trimmedJwksUri
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -1003,14 +1009,10 @@ function ClientGeneralPage() {
|
|||||||
id_token_claims: normalizedIdTokenClaims,
|
id_token_claims: normalizedIdTokenClaims,
|
||||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||||
headless_login_enabled: headlessLoginEnabled,
|
headless_login_enabled: headlessLoginEnabled,
|
||||||
headless_token_endpoint_auth_method:
|
headless_token_endpoint_auth_method: headlessLoginEnabled
|
||||||
clientType === "pkce" && headlessLoginEnabled
|
? tokenEndpointAuthMethod
|
||||||
? tokenEndpointAuthMethod
|
: undefined,
|
||||||
: undefined,
|
headless_jwks_uri: headlessLoginEnabled ? trimmedJwksUri : undefined,
|
||||||
headless_jwks_uri:
|
|
||||||
clientType === "pkce" && headlessLoginEnabled
|
|
||||||
? trimmedJwksUri
|
|
||||||
: undefined,
|
|
||||||
tenant_access_restricted: tenantAccessRestricted,
|
tenant_access_restricted: tenantAccessRestricted,
|
||||||
allowed_tenants: tenantAccessRestricted
|
allowed_tenants: tenantAccessRestricted
|
||||||
? normalizedAllowedTenantIds
|
? normalizedAllowedTenantIds
|
||||||
@@ -2291,6 +2293,38 @@ function ClientGeneralPage() {
|
|||||||
<span className="absolute right-4 top-4 text-primary">
|
<span className="absolute right-4 top-4 text-primary">
|
||||||
{securityProfile === "private" ? "✓" : ""}
|
{securityProfile === "private" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{securityProfile === "private" && (
|
||||||
|
<div
|
||||||
|
className="mt-4 flex items-center justify-between border-t border-primary/20 pt-4"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label
|
||||||
|
className="cursor-pointer text-xs font-bold"
|
||||||
|
htmlFor="headless-login-toggle"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.dev.clients.general.security.headless_login_enable",
|
||||||
|
"Headless Login (자체 로그인 UI 사용)",
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.general.security.headless_login_enable_help",
|
||||||
|
"Baron SSO 로그인 창 대신 RP 자체 로그인 UI를 사용하고, RP backend의 서명 키로 클라이언트를 검증하려는 경우 활성화합니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="headless-login-toggle"
|
||||||
|
checked={headlessLoginEnabled}
|
||||||
|
onCheckedChange={handleHeadlessToggle}
|
||||||
|
disabled={isGeneralSettingsReadOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
@@ -2321,45 +2355,13 @@ function ClientGeneralPage() {
|
|||||||
<span className="absolute right-4 top-4 text-primary">
|
<span className="absolute right-4 top-4 text-primary">
|
||||||
{securityProfile === "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="headless-login-toggle"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"ui.dev.clients.general.security.headless_login_enable",
|
|
||||||
"Headless Login (자체 로그인 UI 사용)",
|
|
||||||
)}
|
|
||||||
</Label>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"ui.dev.clients.general.security.headless_login_enable_help",
|
|
||||||
"Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="headless-login-toggle"
|
|
||||||
checked={headlessLoginEnabled}
|
|
||||||
onCheckedChange={handleHeadlessToggle}
|
|
||||||
disabled={isGeneralSettingsReadOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 4. Public Key Registration (Headless Login) */}
|
{/* 4. Public Key Registration (Headless Login) */}
|
||||||
{clientType === "pkce" && headlessLoginEnabled && (
|
{headlessLoginEnabled && (
|
||||||
<Card className="glass-panel border-primary/20">
|
<Card className="glass-panel border-primary/20">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
@@ -2373,7 +2375,7 @@ function ClientGeneralPage() {
|
|||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t(
|
{t(
|
||||||
"msg.dev.clients.general.public_key.subtitle",
|
"msg.dev.clients.general.public_key.subtitle",
|
||||||
"Headless Login 판정에 필요한 공개키와 관련 설정을 관리합니다.",
|
"Server side App의 Headless Login capability에 필요한 공개키와 검증 정보를 관리합니다.",
|
||||||
)}
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
@@ -2392,7 +2394,7 @@ function ClientGeneralPage() {
|
|||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
"msg.dev.clients.general.public_key.headless_help",
|
"msg.dev.clients.general.public_key.headless_help",
|
||||||
"애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.",
|
"RP가 자체 로그인 UI를 제공하더라도 실제 인증 흐름은 Baron API와 RP backend의 signed key 검증을 통해 이어집니다.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -524,18 +524,28 @@ function ClientsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
variant={client.type === "private" ? "success" : "muted"}
|
<Badge
|
||||||
>
|
variant={
|
||||||
{client.type === "private"
|
client.type === "private" ||
|
||||||
? t("ui.dev.clients.type.private", "Server side App")
|
client.metadata?.headless_login_enabled
|
||||||
: client.metadata?.headless_login_enabled
|
? "success"
|
||||||
|
: "muted"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{client.metadata?.headless_login_enabled
|
||||||
? t(
|
? t(
|
||||||
"ui.dev.clients.type.pkce_headless",
|
"ui.dev.clients.type.private_headless",
|
||||||
"PKCE (Headless Login)",
|
"Server side App (Headless Login)",
|
||||||
)
|
)
|
||||||
: t("ui.dev.clients.type.pkce", "PKCE")}
|
: client.type === "private"
|
||||||
</Badge>
|
? t(
|
||||||
|
"ui.dev.clients.type.private",
|
||||||
|
"Server side App",
|
||||||
|
)
|
||||||
|
: t("ui.dev.clients.type.pkce", "PKCE")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ save_error = "Save Error"
|
|||||||
save_forbidden = "You do not have permission to edit this RP. Ask an administrator to grant RP General Settings or RP Admin relationship."
|
save_forbidden = "You do not have permission to edit this RP. Ask an administrator to grant RP General Settings or RP Admin relationship."
|
||||||
secret_rotated = "Secret Rotated"
|
secret_rotated = "Secret Rotated"
|
||||||
secret_not_applicable = "PKCE apps do not have a client secret."
|
secret_not_applicable = "PKCE apps do not have a client secret."
|
||||||
|
secret_not_applicable_headless = "This app uses signed key authentication for Headless Login, so it does not use a client secret."
|
||||||
secret_unavailable = "SECRET_NOT_AVAILABLE"
|
secret_unavailable = "SECRET_NOT_AVAILABLE"
|
||||||
subtitle = "Manage OIDC credentials and endpoints."
|
subtitle = "Manage OIDC credentials and endpoints."
|
||||||
|
|
||||||
@@ -1545,7 +1546,7 @@ pkce = "PKCE"
|
|||||||
headless_login = "Headless Login"
|
headless_login = "Headless Login"
|
||||||
title = "Security Settings"
|
title = "Security Settings"
|
||||||
headless_login_enable = "Headless Login (Custom Login UI)"
|
headless_login_enable = "Headless Login (Custom Login UI)"
|
||||||
headless_login_enable_help = "Enable this if you want to implement your own login screen within the app instead of using the Baron SSO login page."
|
headless_login_enable_help = "Enable this when the RP uses its own login UI and the RP backend proves the client with signed keys instead of the Baron SSO login page."
|
||||||
|
|
||||||
[ui.dev.clients.general.public_key]
|
[ui.dev.clients.general.public_key]
|
||||||
auth_method = "Token Endpoint Auth Method"
|
auth_method = "Token Endpoint Auth Method"
|
||||||
@@ -1688,6 +1689,7 @@ type = "Type"
|
|||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
private = "Server side App"
|
private = "Server side App"
|
||||||
pkce_headless = "PKCE (Headless Login)"
|
pkce_headless = "PKCE (Headless Login)"
|
||||||
|
private_headless = "Server side App (Headless Login)"
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = "devfront ready"
|
ready_badge = "devfront ready"
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ save_error = "저장 실패: {{error}}"
|
|||||||
save_forbidden = "이 RP 설정을 수정할 권한이 없습니다.\n관리자에게 RP 일반 설정 또는 RP 관리자 관계 부여를 요청해 주세요."
|
save_forbidden = "이 RP 설정을 수정할 권한이 없습니다.\n관리자에게 RP 일반 설정 또는 RP 관리자 관계 부여를 요청해 주세요."
|
||||||
secret_rotated = "Client Secret이 재발급되었습니다."
|
secret_rotated = "Client Secret이 재발급되었습니다."
|
||||||
secret_not_applicable = "PKCE 앱에는 Client Secret이 없습니다."
|
secret_not_applicable = "PKCE 앱에는 Client Secret이 없습니다."
|
||||||
|
secret_not_applicable_headless = "이 앱은 Headless Login용 signed key 인증을 사용하므로 Client Secret을 사용하지 않습니다."
|
||||||
secret_unavailable = "SECRET_NOT_AVAILABLE"
|
secret_unavailable = "SECRET_NOT_AVAILABLE"
|
||||||
subtitle = "OIDC 자격 증명과 엔드포인트를 관리합니다."
|
subtitle = "OIDC 자격 증명과 엔드포인트를 관리합니다."
|
||||||
|
|
||||||
@@ -1543,7 +1544,7 @@ private = "Server side App"
|
|||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
title = "보안 설정"
|
title = "보안 설정"
|
||||||
headless_login_enable = "Headless Login (자체 로그인 UI 사용)"
|
headless_login_enable = "Headless Login (자체 로그인 UI 사용)"
|
||||||
headless_login_enable_help = "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다."
|
headless_login_enable_help = "Baron SSO 로그인 창 대신 RP 자체 로그인 UI를 사용하고, RP backend의 서명 키로 클라이언트를 검증하려는 경우 활성화합니다."
|
||||||
|
|
||||||
|
|
||||||
[ui.dev.clients.general.public_key]
|
[ui.dev.clients.general.public_key]
|
||||||
@@ -1687,6 +1688,7 @@ type = "유형"
|
|||||||
private = "Server side App"
|
private = "Server side App"
|
||||||
pkce = "PKCE"
|
pkce = "PKCE"
|
||||||
pkce_headless = "PKCE (Headless Login)"
|
pkce_headless = "PKCE (Headless Login)"
|
||||||
|
private_headless = "Server side App (Headless Login)"
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = "devfront ready"
|
ready_badge = "devfront ready"
|
||||||
|
|||||||
@@ -414,6 +414,7 @@ save_error = ""
|
|||||||
save_forbidden = ""
|
save_forbidden = ""
|
||||||
secret_rotated = ""
|
secret_rotated = ""
|
||||||
secret_not_applicable = ""
|
secret_not_applicable = ""
|
||||||
|
secret_not_applicable_headless = ""
|
||||||
secret_unavailable = ""
|
secret_unavailable = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
@@ -1744,6 +1745,7 @@ type = ""
|
|||||||
pkce = ""
|
pkce = ""
|
||||||
private = ""
|
private = ""
|
||||||
pkce_headless = ""
|
pkce_headless = ""
|
||||||
|
private_headless = ""
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = ""
|
ready_badge = ""
|
||||||
|
|||||||
@@ -282,16 +282,18 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
).toHaveValue("2");
|
).toHaveValue("2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pkce headless login uses jwks uri only and shows cache actions", async ({
|
test("headless login uses jwks uri only and shows cache actions", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const state = {
|
const state = {
|
||||||
clients: [
|
clients: [
|
||||||
makeClient("client-headless-login", {
|
makeClient("client-headless-login", {
|
||||||
name: "Headless Login App",
|
name: "Headless Login App",
|
||||||
type: "pkce",
|
type: "private",
|
||||||
metadata: {
|
metadata: {
|
||||||
request_object_signing_alg: "RS256",
|
headless_login_enabled: true,
|
||||||
|
headless_token_endpoint_auth_method: "private_key_jwt",
|
||||||
|
headless_jwks_uri: jwksUri,
|
||||||
},
|
},
|
||||||
headlessJwksCache: {
|
headlessJwksCache: {
|
||||||
clientId: "client-headless-login",
|
clientId: "client-headless-login",
|
||||||
@@ -338,12 +340,6 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
|
|
||||||
await page.goto("/clients/client-headless-login/settings");
|
await page.goto("/clients/client-headless-login/settings");
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole("switch", {
|
|
||||||
name: /Headless Login \(자체 로그인 UI 사용\)|Headless Login \(Custom Login UI\)/i,
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", {
|
page.getByRole("heading", {
|
||||||
name: /공개키 등록|Public Key Registration/i,
|
name: /공개키 등록|Public Key Registration/i,
|
||||||
@@ -363,7 +359,7 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(() => state.clients[0]?.tokenEndpointAuthMethod)
|
.poll(() => state.clients[0]?.tokenEndpointAuthMethod)
|
||||||
.toBe("none");
|
.toBe("private_key_jwt");
|
||||||
await expect
|
await expect
|
||||||
.poll(() => state.clients[0]?.metadata?.headless_login_enabled)
|
.poll(() => state.clients[0]?.metadata?.headless_login_enabled)
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
@@ -455,17 +451,18 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
.toBe(autoLoginUrl);
|
.toBe(autoLoginUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pkce headless login blocks save when parsed jwks algorithm is unsupported", async ({
|
test("headless login blocks save when parsed jwks algorithm is unsupported", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const state = {
|
const state = {
|
||||||
clients: [
|
clients: [
|
||||||
makeClient("client-headless-unsupported", {
|
makeClient("client-headless-unsupported", {
|
||||||
name: "Unsupported Headless Login App",
|
name: "Unsupported Headless Login App",
|
||||||
type: "pkce",
|
type: "private",
|
||||||
metadata: {
|
metadata: {
|
||||||
headless_login_enabled: true,
|
headless_login_enabled: true,
|
||||||
request_object_signing_alg: "RS256",
|
headless_token_endpoint_auth_method: "private_key_jwt",
|
||||||
|
headless_jwks_uri: jwksUri,
|
||||||
},
|
},
|
||||||
headlessJwksCache: {
|
headlessJwksCache: {
|
||||||
clientId: "client-headless-unsupported",
|
clientId: "client-headless-unsupported",
|
||||||
@@ -511,16 +508,17 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
).toBeDisabled();
|
).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pkce headless login blocks save when parsed jwks algorithm is missing", async ({
|
test("headless login blocks save when parsed jwks algorithm is missing", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const state = {
|
const state = {
|
||||||
clients: [
|
clients: [
|
||||||
makeClient("client-headless-missing-alg", {
|
makeClient("client-headless-missing-alg", {
|
||||||
name: "Missing Alg Headless Login App",
|
name: "Missing Alg Headless Login App",
|
||||||
type: "pkce",
|
type: "private",
|
||||||
metadata: {
|
metadata: {
|
||||||
headless_login_enabled: true,
|
headless_login_enabled: true,
|
||||||
|
headless_token_endpoint_auth_method: "private_key_jwt",
|
||||||
headless_jwks_uri: jwksUri,
|
headless_jwks_uri: jwksUri,
|
||||||
},
|
},
|
||||||
headlessJwksCache: {
|
headlessJwksCache: {
|
||||||
|
|||||||
Reference in New Issue
Block a user