forked from baron/baron-sso
consent 페이지 반복 노출 현상 수정
This commit is contained in:
@@ -23,6 +23,7 @@ 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"`
|
||||||
|
SkipConsent *bool `json:"skip_consent,omitempty"`
|
||||||
JWKSUri string `json:"jwks_uri,omitempty"`
|
JWKSUri string `json:"jwks_uri,omitempty"`
|
||||||
JWKS interface{} `json:"jwks,omitempty"`
|
JWKS interface{} `json:"jwks,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ type clientSummary struct {
|
|||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
ClientSecret string `json:"clientSecret,omitempty"`
|
ClientSecret string `json:"clientSecret,omitempty"`
|
||||||
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
|
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
|
||||||
|
SkipConsent bool `json:"skipConsent"`
|
||||||
JwksUri string `json:"jwksUri,omitempty"`
|
JwksUri string `json:"jwksUri,omitempty"`
|
||||||
Jwks interface{} `json:"jwks,omitempty"`
|
Jwks interface{} `json:"jwks,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
@@ -185,6 +186,7 @@ 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"`
|
||||||
|
SkipConsent *bool `json:"skipConsent"`
|
||||||
JwksUri *string `json:"jwksUri"`
|
JwksUri *string `json:"jwksUri"`
|
||||||
Jwks interface{} `json:"jwks"`
|
Jwks interface{} `json:"jwks"`
|
||||||
Metadata *map[string]interface{} `json:"metadata"`
|
Metadata *map[string]interface{} `json:"metadata"`
|
||||||
@@ -1554,6 +1556,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
ResponseTypes: responseTypes,
|
ResponseTypes: responseTypes,
|
||||||
Scope: strings.Join(scopes, " "),
|
Scope: strings.Join(scopes, " "),
|
||||||
TokenEndpointAuthMethod: tokenAuthMethod,
|
TokenEndpointAuthMethod: tokenAuthMethod,
|
||||||
|
SkipConsent: boolPtr(valueOrBool(req.SkipConsent, true)),
|
||||||
JWKSUri: jwksURI,
|
JWKSUri: jwksURI,
|
||||||
JWKS: jwks,
|
JWKS: jwks,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
@@ -1737,6 +1740,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
resolvedJWKS,
|
resolvedJWKS,
|
||||||
metadata,
|
metadata,
|
||||||
)
|
)
|
||||||
|
resolvedSkipConsent := valueOrBool(req.SkipConsent, valueOrBool(current.SkipConsent, true))
|
||||||
|
|
||||||
updated := domain.HydraClient{
|
updated := domain.HydraClient{
|
||||||
ClientID: current.ClientID,
|
ClientID: current.ClientID,
|
||||||
@@ -1746,6 +1750,7 @@ 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: resolvedTokenAuthMethod,
|
TokenEndpointAuthMethod: resolvedTokenAuthMethod,
|
||||||
|
SkipConsent: boolPtr(resolvedSkipConsent),
|
||||||
JWKSUri: resolvedJWKSURI,
|
JWKSUri: resolvedJWKSURI,
|
||||||
JWKS: resolvedJWKS,
|
JWKS: resolvedJWKS,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
@@ -2491,6 +2496,7 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
|||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
|
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
|
||||||
|
SkipConsent: valueOrBool(client.SkipConsent, true),
|
||||||
JwksUri: client.JWKSUri,
|
JwksUri: client.JWKSUri,
|
||||||
Jwks: client.JWKS,
|
Jwks: client.JWKS,
|
||||||
Metadata: client.Metadata,
|
Metadata: client.Metadata,
|
||||||
@@ -2610,6 +2616,17 @@ func valueOr(ptr *string, fallback string) string {
|
|||||||
return *ptr
|
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 {
|
func valueOrSlice(ptr *[]string, fallback []string) []string {
|
||||||
if ptr == nil {
|
if ptr == nil {
|
||||||
return fallback
|
return fallback
|
||||||
|
|||||||
@@ -1245,6 +1245,117 @@ func TestCreateClient_HeadlessLoginPayloadMapping(t *testing.T) {
|
|||||||
assert.False(t, hasRequestObjectAlg)
|
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) {
|
func TestCreateClient_HeadlessLoginRejectsInlineJWKS(t *testing.T) {
|
||||||
var hydraCalled bool
|
var hydraCalled bool
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
@@ -1394,6 +1505,75 @@ func TestUpdateClient_HeadlessLoginPayloadMapping(t *testing.T) {
|
|||||||
assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
|
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: ¤tSkipConsent,
|
||||||
|
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) {
|
func TestUpdateClient_HeadlessLoginIgnoresExistingTopLevelJWKS(t *testing.T) {
|
||||||
var captured domain.HydraClient
|
var captured domain.HydraClient
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@
|
|||||||
| `backend/internal/handler/auth_handler_test.go:209` | `TestCompletePasswordReset_InvalidTokenRejectedEvenWhenLoginIDExists` | 오류/예외/거부 경로 검증 |
|
| `backend/internal/handler/auth_handler_test.go:209` | `TestCompletePasswordReset_InvalidTokenRejectedEvenWhenLoginIDExists` | 오류/예외/거부 경로 검증 |
|
||||||
| `backend/internal/handler/auth_handler_test.go:249` | `TestProcessPasswordResetToken_EncodesLoginIDInRedirect` | 리다이렉트/쿼리 보존 규칙 검증 |
|
| `backend/internal/handler/auth_handler_test.go:249` | `TestProcessPasswordResetToken_EncodesLoginIDInRedirect` | 리다이렉트/쿼리 보존 규칙 검증 |
|
||||||
| `backend/internal/handler/dev_handler_test.go:103` | `TestCreateClient_Success` | Hydra/RP 연동 검증 |
|
| `backend/internal/handler/dev_handler_test.go:103` | `TestCreateClient_Success` | Hydra/RP 연동 검증 |
|
||||||
|
| `backend/internal/handler/dev_handler_test.go:1248` | `TestCreateClient_DefaultsSkipConsentToTrue` | Hydra RP 생성 시 `skip_consent` 기본값 검증 |
|
||||||
|
| `backend/internal/handler/dev_handler_test.go:1303` | `TestCreateClient_AllowsExplicitSkipConsentFalse` | Hydra RP 생성 시 명시적 consent 요구 설정 보존 검증 |
|
||||||
|
| `backend/internal/handler/dev_handler_test.go:1508` | `TestUpdateClient_AllowsExplicitSkipConsentFalse` | Hydra RP 수정 시 명시적 consent 요구 설정 보존 검증 |
|
||||||
| `backend/internal/handler/dev_handler_test.go:15` | `TestListClients_Success` | Hydra/RP 연동 검증 |
|
| `backend/internal/handler/dev_handler_test.go:15` | `TestListClients_Success` | Hydra/RP 연동 검증 |
|
||||||
| `backend/internal/handler/dev_handler_test.go:49` | `TestGetClient_Success` | Hydra/RP 연동 검증 |
|
| `backend/internal/handler/dev_handler_test.go:49` | `TestGetClient_Success` | Hydra/RP 연동 검증 |
|
||||||
| `backend/internal/handler/dev_handler_test.go:83` | `TestGetClient_NotFound` | 오류/예외/거부 경로 검증 |
|
| `backend/internal/handler/dev_handler_test.go:83` | `TestGetClient_NotFound` | 오류/예외/거부 경로 검증 |
|
||||||
|
|||||||
36
docs/trouble-shooting/issue-614-skip-consent.md
Normal file
36
docs/trouble-shooting/issue-614-skip-consent.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Issue #614 일반 RP Consent 반복 노출
|
||||||
|
|
||||||
|
## 현상
|
||||||
|
- `https://ssob.hmac.kr/`의 나의 App 현황에서 일반 서비스 클라이언트 바로가기를 열 때 Hydra consent 화면이 매번 노출되었습니다.
|
||||||
|
- `DevFront`, `AdminFront`는 동일 경로에서 consent 화면이 반복 노출되지 않았습니다.
|
||||||
|
|
||||||
|
## 원인
|
||||||
|
- 일반 RP 생성/수정 API가 Hydra OAuth2 client의 `skip_consent` 값을 전달하지 않았습니다.
|
||||||
|
- 백엔드 DTO와 DevFront 설정 모델에도 해당 필드가 없어 신규/기존 RP를 신뢰 앱으로 제어할 수 없었습니다.
|
||||||
|
- `remember: true` consent 세션은 이미 적용되어 있었지만, Hydra client 자체의 `skip_consent`와는 별도 정책입니다.
|
||||||
|
|
||||||
|
## 조치
|
||||||
|
- `domain.HydraClient`에 `skip_consent` JSON 필드를 추가했습니다.
|
||||||
|
- Dev API는 `skipConsent` 요청 값을 받을 수 있지만, DevFront UI에는 별도 체크박스를 추가하지 않습니다.
|
||||||
|
- 신규 RP 생성 시 `skipConsent`가 생략되면 기본값을 `true`로 Hydra에 전달합니다.
|
||||||
|
- 기존 RP 수정 시 현재 값이 없으면 `true`로 보정하고, 명시적으로 `false`를 선택하면 그대로 Hydra에 전달합니다.
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
- `TestCreateClient_DefaultsSkipConsentToTrue`
|
||||||
|
- 신규 RP 생성 요청에서 `skipConsent`가 생략되어도 Hydra payload의 `skip_consent`가 `true`인지 검증합니다.
|
||||||
|
- `TestCreateClient_AllowsExplicitSkipConsentFalse`
|
||||||
|
- 신규 RP 생성 요청에서 명시한 `skipConsent: false`가 Hydra payload에 보존되는지 검증합니다.
|
||||||
|
- `TestUpdateClient_AllowsExplicitSkipConsentFalse`
|
||||||
|
- 기존 RP 수정 요청에서 `skipConsent: false`가 Hydra update payload에 보존되는지 검증합니다.
|
||||||
|
|
||||||
|
## 실행 결과
|
||||||
|
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler -run 'Test(CreateClient_(DefaultsSkipConsentToTrue|AllowsExplicitSkipConsentFalse)|UpdateClient_AllowsExplicitSkipConsentFalse)' -count=1`
|
||||||
|
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler -count=1`
|
||||||
|
- `cd devfront && npx biome check src/features/clients/ClientGeneralPage.tsx src/lib/devApi.ts src/locales/en.toml src/locales/ko.toml src/locales/template.toml --formatter-enabled=false --organize-imports-enabled=false`
|
||||||
|
- `cd devfront && npx tsc -b --pretty false`
|
||||||
|
- `node tools/i18n-scanner/index.js`
|
||||||
|
- `node tools/i18n-scanner/value-check.js`
|
||||||
|
|
||||||
|
## 수동 테스트용 RP
|
||||||
|
- `tools/consent-demo-page/index.php`를 추가했습니다.
|
||||||
|
- DevFront에서 테스트용 RP를 따로 만들고, 데모 페이지의 `.env`에 해당 `CLIENT_ID`와 `REDIRECT_URI`를 설정하면 브라우저 기반 OIDC/consent 흐름을 확인할 수 있습니다.
|
||||||
Reference in New Issue
Block a user