From 4e068f76a74c1127e3600837847fdf2706bd510a Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 3 Feb 2026 15:03:06 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=8F=99=EC=9D=98=20?= =?UTF-8?q?=EA=B1=B0=EB=B6=80=20api=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20hyd?= =?UTF-8?q?ra=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/main.go | 6 +- backend/internal/handler/auth_handler.go | 103 ++++++------------ .../internal/service/hydra_admin_service.go | 48 +++++++- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index ac7de5b3..35215a82 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -491,8 +491,10 @@ func main() { auth.Post("/login/code/verify", authHandler.VerifyLoginCode) auth.Post("/login/code/verify-short", authHandler.VerifyLoginShortCode) auth.Post("/password/login", authHandler.PasswordLogin) - auth.Get("/consent", authHandler.GetConsentRequest) - auth.Post("/consent/accept", authHandler.AcceptConsentRequest) + auth.Get("/consent", authHandler.GetConsentRequest) + auth.Post("/consent/accept", authHandler.AcceptConsentRequest) + auth.Post("/consent/reject", authHandler.RejectConsentRequest) + auth.Post("/oidc/login/accept", authHandler.AcceptOidcLoginRequest) auth.Post("/enchanted-link/init", authHandler.InitEnchantedLink) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index ecc25184..b9dda06d 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -3359,104 +3359,50 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { return c.JSON(response) } -// AcceptConsentRequest - 프론트엔드에서 동의한 내용을 바탕으로 Hydra에 승인을 요청합니다. - func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error { - var req struct { - ConsentChallenge string `json:"consent_challenge"` - GrantScope []string `json:"grant_scope"` // 사용자가 선택한 스코프 - } - - if err := c.BodyParser(&req); err != nil { - - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - - } - - - - if reqJson, err := json.Marshal(req); err == nil { - - slog.Info("AcceptConsentRequest: received request body", "body", string(reqJson)) - - } else { - - slog.Error("AcceptConsentRequest: failed to marshal request for logging", "error", err) - - } - - - - if req.ConsentChallenge == "" { - - return fiber.NewError(fiber.StatusBadRequest, "consent_challenge is required") - + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } + if reqJson, err := json.Marshal(req); err == nil { + slog.Info("AcceptConsentRequest: received request body", "body", string(reqJson)) + } else { + slog.Error("AcceptConsentRequest: failed to marshal request for logging", "error", err) + } + if req.ConsentChallenge == "" { + return fiber.NewError(fiber.StatusBadRequest, "consent_challenge is required") + } // 1. Hydra에서 원래 요청 정보 조회 - consentRequest, err := h.Hydra.GetConsentRequest(c.Context(), req.ConsentChallenge) - if err != nil { - slog.Error("failed to get hydra consent request before accepting", "error", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information") - } - - // 2. 스코프 필터링 (사용자가 선택한 것만 허용) - - // 만약 프론트엔드에서 grant_scope를 보내지 않았다면(기존 동작), 전체 허용으로 간주하거나 에러 처리. - - // 여기서는 명시적으로 보낸 경우에만 필터링하고, 없으면 다 승인(하위 호환)하도록 함. - if len(req.GrantScope) > 0 { - - // 유효성 검증: 사용자가 선택한 스코프가 실제로 요청된 스코프에 포함되는지 확인 - allowedScopes := make(map[string]bool) - for _, s := range consentRequest.RequestedScope { - allowedScopes[s] = true - } - - filteredScopes := make([]string, 0, len(req.GrantScope)) - for _, s := range req.GrantScope { - if allowedScopes[s] { - filteredScopes = append(filteredScopes, s) - } - } - - // 덮어씌우기 (Hydra 서비스는 이 필드를 grant_scope로 사용함) - consentRequest.RequestedScope = filteredScopes - } - - // 3. Hydra에 승인 요청 - if consentRequest.Subject == "" { return fiber.NewError(fiber.StatusInternalServerError, "Consent subject missing") } @@ -3475,19 +3421,38 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error { acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims) if err != nil { - slog.Error("failed to accept hydra consent request", "error", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept consent request") - } - - return c.JSON(acceptResp) - } +func (h *AuthHandler) RejectConsentRequest(c *fiber.Ctx) error { + var req struct { + ConsentChallenge string `json:"consent_challenge"` + } + + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if req.ConsentChallenge == "" { + return fiber.NewError(fiber.StatusBadRequest, "consent_challenge is required") + } + + slog.Info("RejectConsentRequest called", "challenge", req.ConsentChallenge) + + rejectResp, err := h.Hydra.RejectConsentRequest(c.Context(), req.ConsentChallenge) + if err != nil { + slog.Error("failed to reject hydra consent request", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reject consent request") + } + + return c.JSON(rejectResp) +} + + func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { var req struct { LoginChallenge string `json:"login_challenge"` diff --git a/backend/internal/service/hydra_admin_service.go b/backend/internal/service/hydra_admin_service.go index e4353dae..9a19d1e5 100644 --- a/backend/internal/service/hydra_admin_service.go +++ b/backend/internal/service/hydra_admin_service.go @@ -377,7 +377,11 @@ type AcceptConsentRequestResponse struct { RedirectTo string `json:"redirectTo"` } -func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge string) (*domain.HydraConsentRequest, error) { +type RejectConsentRequestResponse struct { + RedirectTo string `json:"redirectTo"` +} + +func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge string) (*HydraConsentRequest, error) { params := map[string]string{ "consent_challenge": challenge, } @@ -410,6 +414,48 @@ func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge str return &consentReq, nil } +func (s *HydraAdminService) RejectConsentRequest(ctx context.Context, challenge string) (*RejectConsentRequestResponse, error) { + params := map[string]string{ + "consent_challenge": challenge, + } + endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent/reject", params) + if err != nil { + return nil, err + } + + payload := map[string]interface{}{ + "error": "access_denied", + "error_description": "The user decided to reject the consent request.", + } + body, _ := json.Marshal(payload) + + req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("hydra admin: create request for reject consent failed: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := s.httpClient().Do(req) + if err != nil { + return nil, fmt.Errorf("hydra admin: reject consent request failed: %w", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("hydra admin: reject consent failed status=%d body=%s", resp.StatusCode, string(respBody)) + } + + var hydraResp struct { + RedirectTo string `json:"redirect_to"` + } + if err := json.Unmarshal(respBody, &hydraResp); err != nil { + return nil, fmt.Errorf("hydra admin: decode reject consent response failed: %w", err) + } + + return &RejectConsentRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil +} + func (s *HydraAdminService) GetLoginRequest(ctx context.Context, challenge string) (*HydraLoginRequest, error) { params := map[string]string{ "login_challenge": challenge,