From 26180ae5d12b4067186c19a9693f8741265530a4 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 24 Apr 2026 14:21:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?consent=202=EC=B0=A8=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 47 +++++++++++++++++-- .../repository/client_consent_repository.go | 16 +++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 5d2d4dca..1855f851 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -5129,13 +5129,51 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { "scopes", consentRequest.RequestedScope, ) + // [New] 로컬 DB에서 기존 동의 내역 확인 (강제 자동 승인 전략) + // Hydra가 skip을 주지 않더라도, 우리 DB에 이미 기록이 있다면 승인 처리함 + if !consentRequest.Skip && h.ConsentRepo != nil && consentRequest.Subject != "" { + existingConsent, err := h.ConsentRepo.Find(c.Context(), consentRequest.Client.ClientID, consentRequest.Subject) + if err == nil && existingConsent != nil { + // 요청된 스코프가 이미 동의된 스코프 내에 있는지 확인 + allGranted := true + grantedMap := make(map[string]bool) + for _, s := range existingConsent.GrantedScopes { + grantedMap[s] = true + } + for _, s := range consentRequest.RequestedScope { + if !grantedMap[s] { + allGranted = false + break + } + } + + if allGranted { + slog.Info("Auto-approving based on local DB consent record", "subject", consentRequest.Subject, "client", consentRequest.Client.ClientID) + identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject) + if err == nil && identity != nil { + currentSessionID := h.resolveCurrentSessionID(c) + sessionClaims := withOidcSessionMetadata( + buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope), + currentSessionID, + ) + acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims) + if err == nil { + return c.JSON(acceptResp) + } + slog.Error("failed to force auto-accept based on local DB", "error", err) + } + } + } + } + // Hydra가 이전에 동의한 이력이 있어 skip을 권장하는 경우, 즉시 승인 처리 if consentRequest.Skip { identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject) if err != nil || identity == nil { - slog.Error("failed to load identity for skip consent", "error", err, "subject", consentRequest.Subject) + slog.Error("failed to load identity for skip consent", "error", err, "subject", consentRequest.Subject, "client_id", consentRequest.Client.ClientID) // 신원 정보를 가져오지 못하면 자동 승인을 진행할 수 없으므로 일반 흐름(UI 노출)으로 진행 } else { + currentSessionID := h.resolveCurrentSessionID(c) var tenantID string if consentRequest.Client.Metadata != nil { if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok { @@ -5145,7 +5183,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { sessionClaims := withOidcSessionMetadata( buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID), - h.resolveCurrentSessionID(c), + currentSessionID, ) // [Debug] 실제 생성된 클레임 출력 (요청사항 확인용 - 자동 승인 시) @@ -5158,7 +5196,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims) if err != nil { - slog.Error("failed to auto-accept hydra consent request", "error", err) + slog.Error("failed to auto-accept hydra consent request", "error", err, "client_id", consentRequest.Client.ClientID, "subject", consentRequest.Subject) // 자동 승인 실패 시 일반 흐름으로 진행 } else { // [New] Sync to local DB even on auto-accept to ensure data consistency @@ -5177,7 +5215,6 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { "scopes": consentRequest.RequestedScope, "client_name": consentRequest.Client.ClientName, } - currentSessionID := h.resolveCurrentSessionID(c) if currentSessionID != "" { detailsMap["session_id"] = currentSessionID detailsMap["approved_session_id"] = currentSessionID @@ -5199,7 +5236,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { }) } - slog.Info("Consent skipped and auto-accepted", "subject", consentRequest.Subject, "client", consentRequest.Client.ClientID) + slog.Info("Consent skipped and auto-accepted", "subject", consentRequest.Subject, "client", consentRequest.Client.ClientID, "session_id", currentSessionID) return c.JSON(acceptResp) } } diff --git a/backend/internal/repository/client_consent_repository.go b/backend/internal/repository/client_consent_repository.go index 9ca6a8c5..5e38742d 100644 --- a/backend/internal/repository/client_consent_repository.go +++ b/backend/internal/repository/client_consent_repository.go @@ -3,6 +3,7 @@ package repository import ( "baron-sso-backend/internal/domain" "context" + "errors" "gorm.io/gorm" ) @@ -13,6 +14,7 @@ type ClientConsentRepository interface { List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) ListByTenant(ctx context.Context, clientID, tenantID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) ListBySubject(ctx context.Context, subject string) ([]domain.ClientConsent, error) + Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error) } type clientConsentRepo struct { @@ -23,6 +25,20 @@ func NewClientConsentRepository(db *gorm.DB) ClientConsentRepository { return &clientConsentRepo{db: db} } +func (r *clientConsentRepo) Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error) { + var consent domain.ClientConsent + err := r.db.WithContext(ctx).Unscoped(). + Where("client_id = ? AND subject = ?", clientID, subject). + First(&consent).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &consent, nil +} + func (r *clientConsentRepo) Upsert(ctx context.Context, consent *domain.ClientConsent) error { return r.db.WithContext(ctx).Unscoped(). Where("client_id = ? AND subject = ?", consent.ClientID, consent.Subject). From 7fd750b587d9a2ff17a984ff4a95a52801ed3755 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 24 Apr 2026 14:51:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?consent=20=EC=9E=90=EB=8F=99=20=EC=8A=B9?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20tenantID=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 8 +++++++- docker/ory/oathkeeper/rules.active.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 1855f851..f34b1373 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -5152,8 +5152,14 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject) if err == nil && identity != nil { currentSessionID := h.resolveCurrentSessionID(c) + var tenantID string + if consentRequest.Client.Metadata != nil { + if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok { + tenantID = tid + } + } sessionClaims := withOidcSessionMetadata( - buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope), + buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID), currentSessionID, ) acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims) diff --git a/docker/ory/oathkeeper/rules.active.json b/docker/ory/oathkeeper/rules.active.json index fd6bfb2d..4a0735da 100755 --- a/docker/ory/oathkeeper/rules.active.json +++ b/docker/ory/oathkeeper/rules.active.json @@ -156,4 +156,4 @@ "authorizer": { "handler": "allow" }, "mutators": [{ "handler": "noop" }] } -] +] \ No newline at end of file From 081cd6739a6a9b44d2a5b408c746a915904ea351 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 24 Apr 2026 14:59:40 +0900 Subject: [PATCH 3/3] =?UTF-8?q?backend=20code-check=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 1 + .../handler/auth_handler_dynamic_claims_test.go | 1 - backend/internal/handler/common_test.go | 14 +++++++++++++- backend/internal/handler/user_handler.go | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index f34b1373..03d87b7f 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1144,6 +1144,7 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID return claims } + func withOidcSessionMetadata(claims map[string]any, sessionID string) map[string]any { if claims == nil { claims = map[string]any{} diff --git a/backend/internal/handler/auth_handler_dynamic_claims_test.go b/backend/internal/handler/auth_handler_dynamic_claims_test.go index 9628d54f..57d2cdca 100644 --- a/backend/internal/handler/auth_handler_dynamic_claims_test.go +++ b/backend/internal/handler/auth_handler_dynamic_claims_test.go @@ -265,4 +265,3 @@ func TestGetConsentRequest_Skip_DynamicClaims(t *testing.T) { assert.Equal(t, "Security", capturedClaims["department"]) assert.Equal(t, "Officer", capturedClaims["position"]) } - diff --git a/backend/internal/handler/common_test.go b/backend/internal/handler/common_test.go index cf86510a..1ff6fbc2 100644 --- a/backend/internal/handler/common_test.go +++ b/backend/internal/handler/common_test.go @@ -154,7 +154,19 @@ func (m *mockConsentRepo) ListBySubject(ctx context.Context, subject string) ([] } return results, nil } -func (m *mockConsentRepo) Delete(ctx context.Context, clientID, subject string) error { return nil } + +func (m *mockConsentRepo) Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error) { + for _, consent := range m.consents { + if consent.ClientID == clientID && consent.Subject == subject { + found := consent + return &found, nil + } + } + return nil, nil +} + +func (m *mockConsentRepo) Delete(ctx context.Context, subject, clientID string) error { return nil } + func (m *mockConsentRepo) List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) { results := make([]domain.ClientConsentWithTenantInfo, 0, len(m.consents)) for _, consent := range m.consents { diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index a56a0564..4c0f0757 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -323,7 +323,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error { } return c.JSON(h.mapIdentitySummary(c.Context(), *identity)) -} +} func (h *UserHandler) CreateUser(c *fiber.Ctx) error { if h.OryProvider == nil || h.KratosAdmin == nil {