1
0
forked from baron/baron-sso

consent 페이지 반복 노출 현상 수정

This commit is contained in:
2026-04-23 11:33:06 +09:00
parent 991577258b
commit 487ed20286
5 changed files with 237 additions and 0 deletions

View File

@@ -101,6 +101,7 @@ type clientSummary struct {
Scopes []string `json:"scopes"`
ClientSecret string `json:"clientSecret,omitempty"`
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
SkipConsent bool `json:"skipConsent"`
JwksUri string `json:"jwksUri,omitempty"`
Jwks interface{} `json:"jwks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
@@ -185,6 +186,7 @@ type clientUpsertRequest struct {
GrantTypes *[]string `json:"grantTypes"`
ResponseTypes *[]string `json:"responseTypes"`
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
SkipConsent *bool `json:"skipConsent"`
JwksUri *string `json:"jwksUri"`
Jwks interface{} `json:"jwks"`
Metadata *map[string]interface{} `json:"metadata"`
@@ -1554,6 +1556,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
ResponseTypes: responseTypes,
Scope: strings.Join(scopes, " "),
TokenEndpointAuthMethod: tokenAuthMethod,
SkipConsent: boolPtr(valueOrBool(req.SkipConsent, true)),
JWKSUri: jwksURI,
JWKS: jwks,
Metadata: metadata,
@@ -1737,6 +1740,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
resolvedJWKS,
metadata,
)
resolvedSkipConsent := valueOrBool(req.SkipConsent, valueOrBool(current.SkipConsent, true))
updated := domain.HydraClient{
ClientID: current.ClientID,
@@ -1746,6 +1750,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
TokenEndpointAuthMethod: resolvedTokenAuthMethod,
SkipConsent: boolPtr(resolvedSkipConsent),
JWKSUri: resolvedJWKSURI,
JWKS: resolvedJWKS,
Metadata: metadata,
@@ -2491,6 +2496,7 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
Scopes: scopes,
ClientSecret: clientSecret,
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
SkipConsent: valueOrBool(client.SkipConsent, true),
JwksUri: client.JWKSUri,
Jwks: client.JWKS,
Metadata: client.Metadata,
@@ -2610,6 +2616,17 @@ func valueOr(ptr *string, fallback string) string {
return *ptr
}
func boolPtr(value bool) *bool {
return &value
}
func valueOrBool(ptr *bool, fallback bool) bool {
if ptr == nil {
return fallback
}
return *ptr
}
func valueOrSlice(ptr *[]string, fallback []string) []string {
if ptr == nil {
return fallback

View File

@@ -1245,6 +1245,117 @@ func TestCreateClient_HeadlessLoginPayloadMapping(t *testing.T) {
assert.False(t, hasRequestObjectAlg)
}
func TestCreateClient_DefaultsSkipConsentToTrue(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,
"skip_consent": captured.SkipConsent,
"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 App",
"type": "pkce",
"redirectUris": []string{"https://rp.example.com/callback"},
})
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.NotNil(t, captured.SkipConsent)
assert.True(t, *captured.SkipConsent)
}
func TestCreateClient_AllowsExplicitSkipConsentFalse(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,
"skip_consent": captured.SkipConsent,
"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": "Consent Required App",
"type": "pkce",
"skipConsent": false,
"redirectUris": []string{"https://rp.example.com/callback"},
})
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.NotNil(t, captured.SkipConsent)
assert.False(t, *captured.SkipConsent)
}
func TestCreateClient_HeadlessLoginRejectsInlineJWKS(t *testing.T) {
var hydraCalled bool
h := &DevHandler{
@@ -1394,6 +1505,75 @@ func TestUpdateClient_HeadlessLoginPayloadMapping(t *testing.T) {
assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
}
func TestUpdateClient_AllowsExplicitSkipConsentFalse(t *testing.T) {
var captured domain.HydraClient
currentSkipConsent := true
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
return httpJSONAny(r, http.StatusOK, domain.HydraClient{
ClientID: "client-1",
ClientName: "Trusted Before",
RedirectURIs: []string{"https://rp.example.com/callback"},
GrantTypes: []string{"authorization_code", "refresh_token"},
ResponseTypes: []string{"code"},
Scope: "openid profile",
TokenEndpointAuthMethod: "none",
SkipConsent: &currentSkipConsent,
Metadata: map[string]interface{}{"status": "active"},
}), nil
}
if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
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,
"skip_consent": captured.SkipConsent,
"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": "Consent Required After",
"type": "pkce",
"skipConsent": false,
})
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.NotNil(t, captured.SkipConsent)
assert.False(t, *captured.SkipConsent)
}
func TestUpdateClient_HeadlessLoginIgnoresExistingTopLevelJWKS(t *testing.T) {
var captured domain.HydraClient