diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 0b1c84fa..63d8a4c7 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -242,6 +242,7 @@ func main() { app := fiber.New(fiber.Config{ AppName: "Baron SSO Backend", DisableStartupMessage: true, // Clean logs + ReadBufferSize: 32768, // 32KB로 증가 (긴 OIDC 챌린지 대응) // Global Error Handler for Production Masking ErrorHandler: func(c *fiber.Ctx, err error) error { // Default status code @@ -459,6 +460,9 @@ 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.Post("/password/reset/initiate", authHandler.InitiatePasswordReset) // [Changed] Use Interstitial Page for GET to prevent Scanner consumption auth.Get("/password/reset/verify", authHandler.VerifyPasswordResetPage) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 197233d7..bd123dd6 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1266,8 +1266,9 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { ale.Operation = "Auth.Password().SignIn" var req struct { - LoginID string `json:"loginId"` - Password string `json:"password"` + LoginID string `json:"loginId"` + Password string `json:"password"` + LoginChallenge string `json:"login_challenge,omitempty"` } if err := c.BodyParser(&req); err != nil { @@ -1314,6 +1315,21 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { setSessionIDLocal(c, authInfo.SessionToken) ale.Log(slog.LevelInfo, "Login successful", slog.String("provider", h.IdpProvider.Name()), slog.String("subject", authInfo.Subject)) + // --- OIDC 로그인 흐름 처리 --- + if req.LoginChallenge != "" { + slog.Info("OIDC login flow detected", "challenge", req.LoginChallenge) + acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, authInfo.Subject) + if err != nil { + slog.Error("failed to accept hydra login request", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request") + } + slog.Info("Hydra login request accepted", "redirectTo", acceptResp.RedirectTo) + return c.JSON(fiber.Map{ + "redirectTo": acceptResp.RedirectTo, + }) + } + // --- OIDC 로그인 흐름 처리 끝 --- + resp := fiber.Map{ "sessionJwt": authInfo.SessionToken.JWT, "status": "ok", @@ -2897,6 +2913,48 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { return c.JSON(linkedRpListResponse{Items: items}) } +func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { + challenge := c.Query("consent_challenge") + if challenge == "" { + return fiber.NewError(fiber.StatusBadRequest, "consent_challenge is required") + } + + consentRequest, err := h.Hydra.GetConsentRequest(c.Context(), challenge) + if err != nil { + slog.Error("failed to get hydra consent request", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information") + } + + return c.JSON(consentRequest) +} + +func (h *AuthHandler) AcceptConsentRequest(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") + } + + 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") + } + + acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest) + 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) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) { token := h.getBearerToken(c) if token != "" { diff --git a/backend/internal/service/hydra_admin_service.go b/backend/internal/service/hydra_admin_service.go index 6d77cebf..103bc45f 100644 --- a/backend/internal/service/hydra_admin_service.go +++ b/backend/internal/service/hydra_admin_service.go @@ -36,6 +36,15 @@ type HydraClient struct { Metadata map[string]interface{} `json:"metadata,omitempty"` } +type HydraConsentRequest struct { + Challenge string `json:"challenge"` + RequestedScope []string `json:"requested_scope"` + RequestedAudience []string `json:"requested_access_token_audience"` + Skip bool `json:"skip"` + Subject string `json:"subject"` + Client HydraClient `json:"client"` +} + type HydraConsentSession struct { Subject string `json:"subject"` GrantedScope []string `json:"granted_scope"` @@ -347,3 +356,134 @@ func (s *HydraAdminService) buildURLWithParams(path string, params map[string]st u.RawQuery = q.Encode() return u.String(), nil } + +type AcceptLoginRequestResponse struct { + RedirectTo string `json:"redirectTo"` +} + +type AcceptConsentRequestResponse struct { + RedirectTo string `json:"redirectTo"` +} + +func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge string) (*HydraConsentRequest, error) { + params := map[string]string{ + "consent_challenge": challenge, + } + endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent", params) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("hydra admin: create request for get consent failed: %w", err) + } + + resp, err := s.httpClient().Do(req) + if err != nil { + return nil, fmt.Errorf("hydra admin: get consent request failed: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("hydra admin: get consent failed status=%d body=%s", resp.StatusCode, string(body)) + } + + var consentReq HydraConsentRequest + if err := json.Unmarshal(body, &consentReq); err != nil { + return nil, fmt.Errorf("hydra admin: decode get consent response failed: %w", err) + } + + return &consentReq, nil +} + +func (s *HydraAdminService) AcceptConsentRequest(ctx context.Context, challenge string, grantInfo *HydraConsentRequest) (*AcceptConsentRequestResponse, error) { + params := map[string]string{ + "consent_challenge": challenge, + } + endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent/accept", params) + if err != nil { + return nil, err + } + + payload := map[string]interface{}{ + "grant_scope": grantInfo.RequestedScope, + "grant_audience": grantInfo.RequestedAudience, + "remember": true, + "remember_for": 3600, + } + 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 accept 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: accept 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: accept consent failed status=%d body=%s", resp.StatusCode, string(respBody)) + } + + // Hydra 응답(redirect_to)을 읽어서 우리 응답(redirectTo)으로 변환 + var hydraResp struct { + RedirectTo string `json:"redirect_to"` + } + if err := json.Unmarshal(respBody, &hydraResp); err != nil { + return nil, fmt.Errorf("hydra admin: decode accept consent response failed: %w", err) + } + + return &AcceptConsentRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil +} + + +func (s *HydraAdminService) AcceptLoginRequest(ctx context.Context, challenge string, subject string) (*AcceptLoginRequestResponse, error) { + params := map[string]string{ + "login_challenge": challenge, + } + endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login/accept", params) + if err != nil { + return nil, err + } + + payload := map[string]interface{}{ + "subject": subject, + "remember": true, + "remember_for": 3600, + } + 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 accept login 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: accept login request failed: %w", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("hydra admin: accept login failed status=%d body=%s", resp.StatusCode, string(respBody)) + } + + // Hydra 응답(redirect_to)을 읽어서 우리 응답(redirectTo)으로 변환 + var hydraResp struct { + RedirectTo string `json:"redirect_to"` + } + if err := json.Unmarshal(respBody, &hydraResp); err != nil { + return nil, fmt.Errorf("hydra admin: decode accept login response failed: %w", err) + } + + return &AcceptLoginRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil +}