From bf469b1eb4243165014bc3c9b4859e48829141ef Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Tue, 3 Feb 2026 18:10:31 +0900 Subject: [PATCH] Merge origin/main and remove Descope deps --- backend/cmd/server/main.go | 6 - backend/docs/openapi.yaml | 46 -- backend/internal/handler/admin_handler.go | 29 +- backend/internal/handler/auth_handler.go | 521 ++++-------------- backend/internal/handler/auth_handler_test.go | 6 +- .../internal/handler/password_policy_test.go | 154 +----- backend/internal/idp/factory.go | 2 +- backend/internal/logger/audit_logger.go | 18 +- .../lib/core/services/auth_proxy_service.dart | 38 +- .../presentation/dashboard_screen.dart | 10 +- 10 files changed, 172 insertions(+), 658 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 2b96197c..02065c64 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -573,12 +573,6 @@ func main() { dev.Get("/consents", devHandler.ListConsents) dev.Delete("/consents", devHandler.RevokeConsents) - // Webhook for Descope Generic SMS Gateway - auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay) - - // Webhook for Descope Generic Email Gateway (Fake Email Strategy) - auth.Post("/webhooks/descope-email", authHandler.HandleDescopeEmailRelay) - // Webhook for Kratos courier (HTTP delivery) auth.Post("/webhooks/kratos-courier", authHandler.HandleKratosCourierRelay) diff --git a/backend/docs/openapi.yaml b/backend/docs/openapi.yaml index 76df7b00..47068c72 100644 --- a/backend/docs/openapi.yaml +++ b/backend/docs/openapi.yaml @@ -842,34 +842,6 @@ paths: "200": description: OK - /api/v1/auth/webhooks/descope-sms: - post: - tags: [Webhook] - summary: Descope SMS 릴레이 - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/DescopeSmsWebhookRequest" - responses: - "200": - description: OK - - /api/v1/auth/webhooks/descope-email: - post: - tags: [Webhook] - summary: Descope Email 릴레이 - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/DescopeEmailWebhookRequest" - responses: - "200": - description: OK - components: schemas: ErrorResponse: @@ -1456,21 +1428,3 @@ components: data: type: object additionalProperties: true - - DescopeSmsWebhookRequest: - type: object - properties: - recipient: - type: string - body: - type: string - - DescopeEmailWebhookRequest: - type: object - properties: - to: - type: string - subject: - type: string - text: - type: string diff --git a/backend/internal/handler/admin_handler.go b/backend/internal/handler/admin_handler.go index 5511b2b4..04c2805e 100644 --- a/backend/internal/handler/admin_handler.go +++ b/backend/internal/handler/admin_handler.go @@ -1,41 +1,16 @@ package handler import ( - "log/slog" - "os" "runtime" "time" - "github.com/descope/go-sdk/descope/client" "github.com/gofiber/fiber/v2" ) -type AdminHandler struct { - DescopeClient *client.DescopeClient -} +type AdminHandler struct{} func NewAdminHandler() *AdminHandler { - projectID := os.Getenv("DESCOPE_PROJECT_ID") - managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY") - - var descopeClient *client.DescopeClient - var err error - - if projectID != "" && managementKey != "" { - descopeClient, err = client.NewWithConfig(&client.Config{ - ProjectID: projectID, - ManagementKey: managementKey, - }) - if err != nil { - slog.Warn("Failed to initialize Descope Client for Admin", "error", err) - } - } else { - slog.Warn("DESCOPE_PROJECT_ID or DESCOPE_MANAGEMENT_KEY missing. Admin functions will fail.") - } - - return &AdminHandler{ - DescopeClient: descopeClient, - } + return &AdminHandler{} } func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error { diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 25dd8742..b5a06c63 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -25,8 +25,6 @@ import ( "strings" "time" - "github.com/descope/go-sdk/descope" - "github.com/descope/go-sdk/descope/client" "github.com/gofiber/fiber/v2" ) @@ -80,18 +78,16 @@ const ( ) type AuthHandler struct { - ProjectID string SmsService domain.SmsService EmailService domain.EmailService RedisService *service.RedisService - DescopeClient *client.DescopeClient KratosAdmin *service.KratosAdminService IdpProvider domain.IdentityProvider AuditRepo domain.AuditRepository OathkeeperRepo domain.OathkeeperLogRepository Hydra *service.HydraAdminService TenantService service.TenantService - KetoService service.KetoService + KetoService service.KetoService UserRepo repository.UserRepository } @@ -151,34 +147,17 @@ func checkPollInterval(redis *service.RedisService, key string, interval time.Du } func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, userRepo repository.UserRepository) *AuthHandler { - projectID := os.Getenv("DESCOPE_PROJECT_ID") - managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY") - - var descopeClient *client.DescopeClient - var err error - if projectID != "" { - descopeClient, err = client.NewWithConfig(&client.Config{ - ProjectID: projectID, - ManagementKey: managementKey, - }) - if err != nil { - slog.Warn("Failed to initialize Descope Client", "error", err) - } - } - return &AuthHandler{ - ProjectID: projectID, SmsService: service.NewSmsService(), EmailService: service.NewEmailService(), RedisService: redisService, - DescopeClient: descopeClient, KratosAdmin: service.NewKratosAdminService(), IdpProvider: idpProvider, AuditRepo: auditRepo, OathkeeperRepo: oathkeeperRepo, Hydra: service.NewHydraAdminService(), TenantService: tenantService, - KetoService: ketoService, + KetoService: ketoService, UserRepo: userRepo, } } @@ -424,7 +403,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { } else { slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status) // Policy: If tenant exists but not active, reject signup or allow as general? - // For now, let's allow as general but log it. + // For now, let's allow as general but log it. // Or return error if we want strict domain locking. return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."}) } @@ -1528,7 +1507,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { if err := c.BodyParser(&req); err != nil { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Body parse error") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } @@ -1543,7 +1522,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { if h.IdpProvider == nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "IDP Provider is nil" + ale.ProviderError = "IDP Provider is nil" ale.Log(slog.LevelError, "IDP Provider is nil") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"}) } @@ -1555,7 +1534,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { } ale.Status = fiber.StatusUnauthorized ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelWarn, "IDP sign-in failed", slog.String("provider", h.IdpProvider.Name())) if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "identity") { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"}) @@ -1605,7 +1584,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { return c.JSON(resp) } -// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 Descope를 통해 이메일 또는 SMS를 보냅니다. +// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 이메일 또는 SMS를 보냅니다. func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { startTime := time.Now() ale := logger.NewAuditLogEntry(c, "initiate") @@ -1614,7 +1593,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if err := c.BodyParser(&req); err != nil { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Body parse error") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } @@ -1626,7 +1605,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if loginID == "" { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "Login ID is required" + ale.ProviderError = "Login ID is required" ale.Log(slog.LevelWarn, "Login ID missing") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID is required"}) } @@ -1634,7 +1613,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if h.IdpProvider == nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "IDP Provider is not initialized" + ale.ProviderError = "IDP Provider is not initialized" ale.Log(slog.LevelError, "IDP Provider is not initialized") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"}) } @@ -1643,7 +1622,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if userfrontURL == "" { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "USERFRONT_URL is not set" + ale.ProviderError = "USERFRONT_URL is not set" ale.Log(slog.LevelError, "USERFRONT_URL is not set") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "USERFRONT_URL environment variable is not set"}) } @@ -1656,7 +1635,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if resetToken == "" { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "Failed to generate reset token" + ale.ProviderError = "Failed to generate reset token" ale.Log(slog.LevelError, "Failed to generate reset token") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate reset token"}) } @@ -1664,7 +1643,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if err := h.RedisService.Set(prefixPwdResetToken+resetToken, loginID, pwdResetExpiration); err != nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to store reset token in Redis") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to store reset token"}) } @@ -1682,7 +1661,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if !drySend && h.EmailService == nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "Email service not configured" + ale.ProviderError = "Email service not configured" ale.Log(slog.LevelError, "Email service not configured") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) } @@ -1703,7 +1682,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if err := h.EmailService.SendEmail(loginID, subject, body); err != nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to send reset email", slog.String("loginId", loginID)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset email"}) } @@ -1716,7 +1695,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { if err := h.SmsService.SendSms(loginID, resetSms); err != nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to send reset SMS", slog.String("loginId", loginID)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset SMS"}) } @@ -1793,7 +1772,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error { if token == "" { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "Missing token" + ale.ProviderError = "Missing token" ale.Log(slog.LevelWarn, "Missing token in request") return c.Status(fiber.StatusBadRequest).SendString("Missing token") } @@ -1802,7 +1781,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error { if err != nil || loginID == "" { ale.Status = fiber.StatusUnauthorized ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "Invalid or expired reset token" + ale.ProviderError = "Invalid or expired reset token" ale.Log(slog.LevelWarn, "Reset token invalid or expired") return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired token") } @@ -1842,7 +1821,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { if err := c.BodyParser(&req); err != nil { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Body parse error") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } @@ -1875,7 +1854,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { if loginID == "" || req.NewPassword == "" { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "Login ID and new password are required" + ale.ProviderError = "Login ID and new password are required" ale.Log(slog.LevelWarn, "Login ID or new password missing") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID and new password are required"}) } @@ -1887,7 +1866,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { if err := validatePasswordWithPolicy(policy, req.NewPassword); err != nil { ale.Status = fiber.StatusBadRequest ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelWarn, "Validation failed: "+err.Error()) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } @@ -1897,7 +1876,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { if h.IdpProvider == nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = "IDP Provider is nil" + ale.ProviderError = "IDP Provider is nil" ale.Log(slog.LevelError, "IDP Provider is nil") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"}) } @@ -1905,7 +1884,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { if err := h.IdpProvider.UpdateUserPassword(loginID, req.NewPassword, nil); err != nil { ale.Status = fiber.StatusInternalServerError ale.LatencyMs = time.Since(startTime) - ale.DescopeError = err.Error() + ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to update password via IDP") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"}) } @@ -2042,20 +2021,6 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error { } // 2. 모바일 토큰은 승인 검증용으로만 사용하고, 웹 전용 세션을 새로 발급 - if sessionToken, loginID, approvedSessionID, err := h.tryIssueDescopeQrSession(c, req.Token); err != nil { - slog.Error("[QR] Issue web session failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue web session"}) - } else if sessionToken != nil && sessionToken.JWT != "" { - h.storeQrApproverSessionID(pendingRef, approvedSessionID) - h.writeQrAuditLog(loginID, pendingRef, sessionToken, approvedSessionID) - sessionData, _ := json.Marshal(map[string]string{ - "status": statusSuccess, - "jwt": sessionToken.JWT, - }) - h.RedisService.Set(prefixSession+pendingRef, string(sessionData), 5*time.Minute) - return c.JSON(fiber.Map{"message": "QR Login Approved"}) - } - approvedSessionID := "" if req.Token != "" { if sessionID, err := h.getKratosSessionID(req.Token); err == nil { @@ -2079,11 +2044,6 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "QR Login Approved"}) } -// ProxyToDescope (Placeholder) -func (h *AuthHandler) ProxyToDescope(c *fiber.Ctx, path string, payload interface{}) error { - return c.Status(501).SendString("Descope Proxy Disabled") -} - type kratosCourierRequest struct { Recipient string `json:"recipient"` TemplateType string `json:"template_type"` @@ -2480,82 +2440,6 @@ func sanitizePhoneForSms(phone string) string { return sanitized } -// HandleDescopeSmsRelay -func (h *AuthHandler) HandleDescopeSmsRelay(c *fiber.Ctx) error { - var req struct { - Recipient string `json:"recipient"` - Body string `json:"body"` - } - - if err := c.BodyParser(&req); err != nil { - slog.Error("Webhook Body parsing failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) - } - - if req.Recipient == "" || req.Body == "" { - slog.Warn("Webhook missing recipient or body") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing recipient or body"}) - } - - slog.Info("Received SMS request", "recipient", req.Recipient) - - phone := req.Recipient - if strings.HasPrefix(phone, "+82") { - phone = "0" + phone[3:] - } - phone = strings.ReplaceAll(phone, "-", "") - phone = strings.ReplaceAll(phone, " ", "") - - if err := h.SmsService.SendSms(phone, req.Body); err != nil { - slog.Error("Failed to forward SMS to Naver", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS via Naver"}) - } - - slog.Info("Successfully forwarded SMS", "phone", phone) - return c.JSON(fiber.Map{"status": "ok"}) -} - -// HandleDescopeEmailRelay - Webhook for Descope Generic Email Gateway -// Used for "Fake Email Strategy" to support Polling with SMS. -func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error { - var req struct { - To string `json:"to"` // e.g., 01012345678@sms.baron - Subject string `json:"subject"` - Text string `json:"text"` // Body containing the link - } - - if err := c.BodyParser(&req); err != nil { - slog.Error("[Email Webhook] Body parsing failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) - } - - slog.Info("[Email Webhook] Received email request", "to", req.To) - - // Check if it's a Fake Email for SMS - if strings.HasSuffix(req.To, "@sms.baron") { - phone := strings.Split(req.To, "@")[0] - - // Sanitize Phone (Descope might sanitize or not, but let's be safe) - if strings.HasPrefix(phone, "+82") { - phone = "0" + phone[3:] - } - - // Send SMS with the text body (Descope template should be optimized for SMS) - if err := h.SmsService.SendSms(phone, req.Text); err != nil { - slog.Error("[Email Webhook] Failed to forward Email-as-SMS", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) - } - - slog.Info("[Email Webhook] Successfully converted Email to SMS", "phone", phone) - return c.JSON(fiber.Map{"status": "ok"}) - } - - // Real Email Handling (Not implemented in this Relay) - // You would need an SMTP service here if you route ALL emails through this relay. - slog.Warn("[Email Webhook] Real email skipped (Not implemented)", "to", req.To) - return c.Status(501).JSON(fiber.Map{"error": "Real email sending not implemented"}) -} - // --- User Profile Handlers --- func (h *AuthHandler) formatPhoneForDisplay(phone string) string { @@ -2581,6 +2465,7 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error { } return c.JSON(profile) } + // GetEnrichedProfile - Exported wrapper for resolveCurrentProfile used by middlewares func (h *AuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) { return h.resolveCurrentProfile(c) @@ -2975,6 +2860,17 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) } + currentSessionID := "" + if token := h.getBearerToken(c); token != "" { + currentSessionID = extractSessionIDFromJWT(token) + } + if currentSessionID == "" { + if cookie := c.Get("Cookie"); cookie != "" { + if sessionID, err := h.getKratosSessionIDWithCookie(cookie); err == nil { + currentSessionID = sessionID + } + } + } subject := "" if h.OathkeeperRepo != nil { @@ -3056,7 +2952,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { if !isAuthEventType(log.EventType) { continue } - if !matchesAuthTimelineUser(log, profile, candidates) { + if !matchesAuthTimelineUser(log, profile, candidates, currentSessionID) { continue } if shouldSkipAuthTimeline(log) { @@ -3467,7 +3363,9 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error { func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { var req struct { - LoginChallenge string `json:"login_challenge"` + LoginChallenge string `json:"login_challenge"` + ApprovedSessionID string `json:"approved_session_id,omitempty"` + SessionID string `json:"session_id,omitempty"` } if err := c.BodyParser(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") @@ -3481,13 +3379,31 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusUnauthorized, "Authentication required") } c.Locals("user_id", subject) - if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" { - c.Locals("approved_session_id", sessionID) - } else if token := h.getBearerToken(c); token != "" { - if derivedID := extractSessionIDFromJWT(token); derivedID != "" { - c.Locals("approved_session_id", derivedID) + approvedSessionID := strings.TrimSpace(req.ApprovedSessionID) + if approvedSessionID == "" { + approvedSessionID = strings.TrimSpace(req.SessionID) + } + if approvedSessionID == "" { + if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" { + approvedSessionID = sessionID } } + if approvedSessionID == "" { + if token := h.getBearerToken(c); token != "" { + approvedSessionID = extractSessionIDFromJWT(token) + } + } + if approvedSessionID == "" { + if cookie := c.Get("Cookie"); cookie != "" { + if derivedID, err := h.getKratosSessionIDWithCookie(cookie); err == nil { + approvedSessionID = derivedID + } + } + } + if approvedSessionID != "" { + c.Locals("session_id", approvedSessionID) + c.Locals("approved_session_id", approvedSessionID) + } if h.KratosAdmin != nil { if identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject); err == nil && identity != nil { if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" { @@ -3506,65 +3422,12 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { } func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) { - // [Development Mode Fallback] - if os.Getenv("APP_ENV") != "production" { - // 우선순위: 1. 헤더, 2. 쿠키, 3. 기본값(user) - testRole := c.Get("X-Test-Role") - if testRole == "" { - testRole = c.Cookies("X-Mock-Role") - } - - if testRole == "" { - testRole = domain.RoleUser // 기본값을 user로 변경하여 차단 확인 - } - - slog.Info("Using MOCK profile", "role", testRole, "source", "dev_fallback") - return &domain.UserProfileResponse{ - ID: "dev-admin-uuid", - Email: "dev-admin@baron.local", - Name: "Dev Admin (" + testRole + ")", - Role: testRole, - CompanyCode: "hanmac", - }, nil - } - var profile *domain.UserProfileResponse var err error token := h.getBearerToken(c) if token != "" { - if looksLikeJWT(token) && h.DescopeClient != nil { - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err == nil && authorized { - userResponse, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID) - if err == nil { - identityID, resolveErr := h.resolveKratosIdentityID( - c.Context(), - userResponse.Email, - normalizePhoneForLoginID(userResponse.Phone), - ) - if resolveErr == nil && identityID != "" { - dept, _ := userResponse.CustomAttributes["department"].(string) - affType, _ := userResponse.CustomAttributes["affiliationType"].(string) - compCode, _ := userResponse.CustomAttributes["companyCode"].(string) - profile = &domain.UserProfileResponse{ - ID: identityID, - Email: userResponse.Email, - Name: userResponse.Name, - Phone: h.formatPhoneForDisplay(userResponse.Phone), - Department: dept, - AffiliationType: affType, - CompanyCode: compCode, - Metadata: userResponse.CustomAttributes, - } - } - } - } - } - - if profile == nil { - profile, err = h.getKratosProfile(token) - } + profile, err = h.getKratosProfile(token) } else { cookie := c.Get("Cookie") if cookie != "" { @@ -3604,23 +3467,6 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) { token := h.getBearerToken(c) if token != "" { - if looksLikeJWT(token) && h.DescopeClient != nil { - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err == nil && authorized { - userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID) - if loadErr == nil { - identityID, resolveErr := h.resolveKratosIdentityID( - c.Context(), - userResponse.Email, - normalizePhoneForLoginID(userResponse.Phone), - ) - if resolveErr == nil { - return identityID, nil - } - } - return "", fmt.Errorf("failed to resolve kratos identity for consent subject") - } - } identityID, resolveErr := h.resolveIdentityID(c, token) if resolveErr == nil && identityID != "" { return identityID, nil @@ -3643,26 +3489,6 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) { func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) { token := h.getBearerToken(c) - if token != "" && looksLikeJWT(token) && h.DescopeClient != nil { - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err == nil && authorized { - subjects := make([]string, 0, 2) - userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID) - if loadErr == nil { - subjects = appendLoginIDsFromValues(subjects, userResponse.Email, userResponse.Phone) - identityID, resolveErr := h.resolveKratosIdentityID( - c.Context(), - userResponse.Email, - normalizePhoneForLoginID(userResponse.Phone), - ) - if resolveErr == nil && identityID != "" { - subjects = append([]string{identityID}, subjects...) - } - } - return uniqueStrings(subjects), nil - } - } - if token != "" { identityID, traits, err := h.getKratosIdentity(token) if err == nil && identityID != "" { @@ -4042,7 +3868,7 @@ func normalizeLoginIdentifier(value string) string { return normalizePhoneForLoginID(trimmed) } -func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileResponse, candidates map[string]struct{}) bool { +func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileResponse, candidates map[string]struct{}, sessionID string) bool { if profile == nil { return false } @@ -4051,11 +3877,24 @@ func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileRes } loginID := extractLoginIDFromAuditDetails(log.Details) normalized := normalizeLoginIdentifier(loginID) - if normalized == "" { + if normalized != "" { + if _, ok := candidates[normalized]; ok { + return true + } + } + if sessionID == "" { return false } - _, ok := candidates[normalized] - return ok + if log.SessionID != "" && log.SessionID == sessionID { + return true + } + if extracted := extractSessionIDFromAuditDetails(log.Details); extracted != "" && extracted == sessionID { + return true + } + if approved := extractApprovedSessionIDFromAuditDetails(log.Details); approved != "" && approved == sessionID { + return true + } + return false } func extractLoginIDFromAuditDetails(details string) string { @@ -4215,52 +4054,36 @@ func extractSessionIDFromAuditDetails(details string) string { return "" } -func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) { - if looksLikeJWT(token) && h.DescopeClient != nil { - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err == nil && authorized { - if h.KratosAdmin == nil { - return "", fmt.Errorf("kratos admin unavailable") - } - userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID) - if loadErr != nil { - return "", loadErr - } - identityID, resolveErr := h.resolveKratosIdentityID( - c.Context(), - userResponse.Email, - normalizePhoneForLoginID(userResponse.Phone), - ) - if resolveErr != nil || identityID == "" { - return "", fmt.Errorf("failed to resolve kratos identity for token") - } - return identityID, nil +func extractApprovedSessionIDFromAuditDetails(details string) string { + if details == "" { + return "" + } + var payload map[string]any + if err := json.Unmarshal([]byte(details), &payload); err != nil { + return "" + } + if raw, ok := payload["approved_session_id"]; ok { + switch value := raw.(type) { + case string: + return value + default: + return fmt.Sprint(value) } } - id, _, err := h.getKratosIdentity(token) - return id, err + if raw, ok := payload["approvedSessionId"]; ok { + switch value := raw.(type) { + case string: + return value + default: + return fmt.Sprint(value) + } + } + return "" } -func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (*domain.Token, string, string, error) { - if !looksLikeJWT(token) || h.DescopeClient == nil { - return nil, "", "", nil - } - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err != nil || !authorized { - return nil, "", "", nil - } - loginID, err := h.resolveDescopeLoginID(c.Context(), userToken) - if err != nil { - return nil, "", "", err - } - authInfo, err := h.IdpProvider.IssueSession(loginID) - if err != nil { - return nil, "", "", err - } - if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { - return nil, "", "", fmt.Errorf("descope issue session returned empty token") - } - return authInfo.SessionToken, loginID, "", nil +func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) { + id, _, err := h.getKratosIdentity(token) + return id, err } func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) { @@ -4408,38 +4231,6 @@ func (h *AuthHandler) startQrCodeLoginForQr(loginID, pendingRef, rawRef string) return nil } -func (h *AuthHandler) resolveDescopeLoginID(ctx context.Context, token *descope.Token) (string, error) { - if token == nil { - return "", fmt.Errorf("descope token is nil") - } - - if loginID := extractLoginIDFromClaims(token.Claims); loginID != "" { - return loginID, nil - } - - if h.DescopeClient == nil { - return "", fmt.Errorf("descope client is nil") - } - - user, err := h.DescopeClient.Management.User().Load(ctx, token.ID) - if err != nil { - return "", err - } - if user == nil { - return "", fmt.Errorf("descope user not found") - } - if loginID := pickPrimaryLoginID(user.LoginIDs); loginID != "" { - return loginID, nil - } - if user.Email != "" { - return user.Email, nil - } - if user.Phone != "" { - return user.Phone, nil - } - return "", fmt.Errorf("descope login id not found") -} - func pickPrimaryLoginID(loginIDs []string) string { for _, id := range loginIDs { if strings.Contains(id, "@") { @@ -4768,93 +4559,6 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } - if token != "" && looksLikeJWT(token) && h.DescopeClient != nil { - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err == nil && authorized { - // 1. Load current user to check changes - currentUser, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load current user"}) - } - identityID, resolveErr := h.resolveKratosIdentityID( - c.Context(), - currentUser.Email, - normalizePhoneForLoginID(currentUser.Phone), - ) - if resolveErr != nil || identityID == "" { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"}) - } - - newPhoneStorage := h.formatPhoneForStorage(req.Phone) - oldPhoneStorage := currentUser.Phone - - slog.Info("[UpdateMe] Checking changes", "userID", identityID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name) - - // 2. Handle Phone Number Change - if newPhoneStorage != "" && newPhoneStorage != oldPhoneStorage { - // Check verification status in Redis - verifyKey := "verify_update_phone:" + identityID + ":" + newPhoneStorage - val, _ := h.RedisService.Get(verifyKey) - if val != "verified" { - slog.Warn("[UpdateMe] Phone verification missing", "key", verifyKey) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "휴대폰 번호 변경을 위해 SMS 인증이 필요합니다."}) - } - - // Update Phone in Descope and mark as verified - slog.Info("[UpdateMe] Updating phone number", "userID", identityID, "newPhone", newPhoneStorage) - _, err = h.DescopeClient.Management.User().UpdatePhone(c.Context(), userToken.ID, newPhoneStorage, true, false) - if err != nil { - slog.Error("Failed to update phone in Descope", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "전화번호 업데이트에 실패했습니다."}) - } - - // If the old phone was used as a LoginID, replace it with the new one - for _, loginID := range currentUser.LoginIDs { - // Normalize for comparison - normID := strings.ReplaceAll(loginID, "+82", "0") - normOld := strings.ReplaceAll(oldPhoneStorage, "+82", "0") - - if loginID == oldPhoneStorage || (normOld != "" && normID == normOld) { - slog.Info("[UpdateMe] Updating LoginID", "old", loginID, "new", newPhoneStorage) - _, err = h.DescopeClient.Management.User().UpdateLoginID(c.Context(), loginID, newPhoneStorage) - if err != nil { - slog.Warn("Failed to update LoginID", "error", err) - } - break - } - } - - // Clear verification after successful update - h.RedisService.Delete(verifyKey) - } - - // 3. Update Name if changed - if req.Name != "" && req.Name != currentUser.Name { - slog.Info("[UpdateMe] Updating display name", "userID", identityID, "newName", req.Name) - _, err = h.DescopeClient.Management.User().UpdateDisplayName(c.Context(), userToken.ID, req.Name) - if err != nil { - slog.Error("Failed to update user name", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "이름 업데이트에 실패했습니다."}) - } - } - - // 4. Update Custom Attributes (Department) - if req.Department != "" { - slog.Info("[UpdateMe] Updating department", "userID", identityID, "dept", req.Department) - if _, err := h.DescopeClient.Management.User().UpdateCustomAttribute(c.Context(), userToken.ID, "department", req.Department); err != nil { - slog.Error("Failed to update department", "error", err) - } - } - - slog.Info("[UpdateMe] Profile update completed successfully", "userID", identityID) - - return c.JSON(fiber.Map{ - "status": "success", - "updatedAt": time.Now().Format(time.RFC3339), - }) - } - } - var ( identityID string traits map[string]interface{} @@ -4928,18 +4632,7 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error { loginID := "" token := h.getBearerToken(c) - if token != "" && looksLikeJWT(token) && h.DescopeClient != nil { - authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token) - if err == nil && authorized { - resolved, err := h.resolveDescopeLoginID(c.Context(), userToken) - if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Failed to resolve login ID"}) - } - loginID = resolved - } - } - - if loginID == "" && token != "" { + if token != "" { if resolved, err := h.resolveKratosLoginID(token); err == nil { loginID = resolved } diff --git a/backend/internal/handler/auth_handler_test.go b/backend/internal/handler/auth_handler_test.go index 1a06bcac..d9fa1227 100644 --- a/backend/internal/handler/auth_handler_test.go +++ b/backend/internal/handler/auth_handler_test.go @@ -77,8 +77,8 @@ func TestCompletePasswordReset_InvalidPasswordPolicy(t *testing.T) { } } -func TestCompletePasswordReset_NilDescopeClient(t *testing.T) { - h := &AuthHandler{} // DescopeClient intentionally nil to hit the configuration error branch +func TestCompletePasswordReset_NilIDPProvider(t *testing.T) { + h := &AuthHandler{} // IdpProvider intentionally nil to hit the configuration error branch app := newTestApp(h) body, _ := json.Marshal(map[string]string{ @@ -95,7 +95,7 @@ func TestCompletePasswordReset_NilDescopeClient(t *testing.T) { defer resp.Body.Close() if resp.StatusCode != http.StatusInternalServerError { - t.Fatalf("expected 500 when Descope client is nil, got %d", resp.StatusCode) + t.Fatalf("expected 500 when IDP provider is nil, got %d", resp.StatusCode) } var got map[string]string diff --git a/backend/internal/handler/password_policy_test.go b/backend/internal/handler/password_policy_test.go index 963ffbed..a17e5e59 100644 --- a/backend/internal/handler/password_policy_test.go +++ b/backend/internal/handler/password_policy_test.go @@ -1,26 +1,16 @@ package handler import ( - "context" + "baron-sso-backend/internal/domain" "crypto/rand" "encoding/binary" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "os" - "strconv" "testing" "unicode" - - "github.com/descope/go-sdk/descope" - "github.com/descope/go-sdk/descope/client" - mocksauth "github.com/descope/go-sdk/descope/tests/mocks/auth" ) // 정책을 받아 필수 요구사항을 모두 포함하는 비밀번호를 생성한다. -func generatePasswordFromPolicy(policy *descope.PasswordPolicy) string { - minLen := int(policy.MinLength) +func generatePasswordFromPolicy(policy *domain.PasswordPolicy) string { + minLen := policy.MinLength if minLen < 8 { minLen = 12 // 안전한 기본값 } @@ -65,29 +55,17 @@ func randomInt(n int) int { } func TestGeneratePasswordUsesNonAlphanumericRequirement(t *testing.T) { - mockAuth := &mocksauth.MockAuthentication{ - MockPassword: &mocksauth.MockPassword{ - PolicyResponse: &descope.PasswordPolicy{ - MinLength: 8, - Lowercase: true, - Uppercase: true, - Number: true, - NonAlphanumeric: true, - }, - }, - } - - policy, err := mockAuth.Password().GetPasswordPolicy(context.Background()) - if err != nil { - t.Fatalf("정책 조회 실패: %v", err) - } - if !policy.NonAlphanumeric { - t.Fatalf("정책에 비영문자 요구사항이 표시되지 않음") + policy := &domain.PasswordPolicy{ + MinLength: 8, + Lowercase: true, + Uppercase: true, + Number: true, + NonAlphanumeric: true, } pwd := generatePasswordFromPolicy(policy) - if len(pwd) < int(policy.MinLength) { + if len(pwd) < policy.MinLength { t.Fatalf("비밀번호 길이가 정책 최소 길이 미만: got %d, want >= %d", len(pwd), policy.MinLength) } @@ -118,115 +96,3 @@ func TestGeneratePasswordUsesNonAlphanumericRequirement(t *testing.T) { t.Fatalf("비영문자 요구사항 미충족: %q", pwd) } } - -// 통합 테스트: 실제 Descope 정책으로 비밀번호를 생성하고 교체 플로우를 검증한다. -// 필요 env: -// DESCOPE_PROJECT_ID, DESCOPE_MANAGEMENT_KEY, TEST_DESCOPE_LOGIN_ID, TEST_DESCOPE_CURRENT_PASSWORD -func TestDescopePasswordPolicyAndChange(t *testing.T) { - projectID := os.Getenv("DESCOPE_PROJECT_ID") - managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY") - loginID := os.Getenv("DESCOPE_TEST_ACCOUNT") - - if projectID == "" || managementKey == "" || loginID == "" { - t.Skip("환경변수(DESCOPE_PROJECT_ID, DESCOPE_MANAGEMENT_KEY, DESCOPE_TEST_ACCOUNT) 미설정으로 통합 테스트 건너뜀") - } - - logf := func(format string, args ...any) { - t.Logf(format, args...) - fmt.Printf(format+"\n", args...) - } - - ctx := context.Background() - cl, err := client.NewWithConfig(&client.Config{ - ProjectID: projectID, - ManagementKey: managementKey, - }) - if err != nil { - t.Fatalf("Descope 클라이언트 초기화 실패: %v", err) - } - - policy, err := cl.Auth.Password().GetPasswordPolicy(ctx) - if err != nil { - t.Fatalf("비밀번호 정책 조회 실패: %v", err) - } - logf("정책: min=%d lower=%v upper=%v number=%v nonAlpha=%v", policy.MinLength, policy.Lowercase, policy.Uppercase, policy.Number, policy.NonAlphanumeric) - - // 테스트 계정이 없으면 생성 - users, _, err := cl.Management.User().SearchAll(ctx, &descope.UserSearchOptions{ - LoginIDs: []string{loginID}, - Limit: 1, - Page: 0, - }) - if err != nil { - t.Fatalf("테스트 계정 검색 실패: %v", err) - } - if len(users) == 0 { - logf("테스트 계정 미존재, 생성 시도: %s", loginID) - if _, err := cl.Management.User().CreateTestUser(ctx, loginID, &descope.UserRequest{ - User: descope.User{ - Email: loginID, - }, - }); err != nil { - t.Fatalf("테스트 계정 생성 실패: %v", err) - } - } else { - logf("테스트 계정 존재 확인: %s", loginID) - } - - // 1) 기초 비밀번호 설정 (알려진 값으로 초기화) - basePassword := generatePasswordFromPolicy(policy) - if err := cl.Management.User().SetActivePassword(ctx, loginID, basePassword); err != nil { - logf("초기 비밀번호 설정 실패: status=%d err=%v", statusFromError(err), err) - t.Fatalf("초기 비밀번호 설정 실패: %v", err) - } - logf("초기 비밀번호 설정 완료: %s", basePassword) - - // 2) 초기 비밀번호 로그인 검증 - wOld := httptest.NewRecorder() - _, err = cl.Auth.Password().SignIn(ctx, loginID, basePassword, wOld) - logf("기초 비밀번호 로그인: status=%d err=%v", statusFromError(err), err) - if err != nil { - t.Fatalf("기초 비밀번호 로그인 실패: %v", err) - } - - // 3) 새 비밀번호 생성 및 변경 - newPassword := generatePasswordFromPolicy(policy) - if newPassword == basePassword { - newPassword = newPassword + "Z9!" - } - logf("새 비밀번호 생성: %s", newPassword) - - if err := cl.Management.User().SetActivePassword(ctx, loginID, newPassword); err != nil { - logf("비밀번호 변경 실패: status=%d err=%v", statusFromError(err), err) - t.Fatalf("비밀번호 변경 실패: %v", err) - } - logf("비밀번호 변경 성공(status=200)") - - // 4) 새 비밀번호로 로그인 확인 - wNew := httptest.NewRecorder() - _, err = cl.Auth.Password().SignIn(ctx, loginID, newPassword, wNew) - logf("새 비밀번호 로그인: status=%d err=%v", statusFromError(err), err) - if err != nil { - t.Fatalf("새 비밀번호 로그인 실패: %v", err) - } -} - -func statusFromError(err error) int { - if err == nil { - return http.StatusOK - } - var de *descope.Error - if errors.As(err, &de) { - if statusRaw, ok := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode]; ok { - switch v := statusRaw.(type) { - case int: - return v - case string: - if n, convErr := strconv.Atoi(v); convErr == nil { - return n - } - } - } - } - return http.StatusInternalServerError -} diff --git a/backend/internal/idp/factory.go b/backend/internal/idp/factory.go index 221e027a..e8ccba55 100644 --- a/backend/internal/idp/factory.go +++ b/backend/internal/idp/factory.go @@ -34,7 +34,7 @@ func getEnv(key, fallback string) string { // InitializeProvider는 환경 설정을 기반으로 IDP 공급자를 생성하고 반환합니다. // 이것은 IdentityProvider 인터페이스의 팩토리 역할을 합니다. func InitializeProvider() (domain.IdentityProvider, error) { - rawProviders := getEnv("IDP_PROVIDER", "descope") // 기본값은 descope입니다. + rawProviders := getEnv("IDP_PROVIDER", "ory") providers := strings.Split(rawProviders, ",") slog.Info("Initializing IDP chain", "providers", rawProviders) diff --git a/backend/internal/logger/audit_logger.go b/backend/internal/logger/audit_logger.go index 50f414a4..e82d1017 100644 --- a/backend/internal/logger/audit_logger.go +++ b/backend/internal/logger/audit_logger.go @@ -27,9 +27,9 @@ type AuditLogEntry struct { Headers map[string]string // Core headers like Host, Cookie, Set-Cookie LoginIDs map[string]string // loginId and loginId_normalized Token string // For reset tokens, magic link tokens - DescopeError string - DescopeStatus int // Descope HTTP status - DescopeBody string // Descope response body (full raw) + ProviderError string + ProviderStatus int // Provider HTTP status + ProviderBody string // Provider response body (full raw) RefreshToken string SessionJwt string AccessJwt string @@ -143,14 +143,14 @@ func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) { if ale.Token != "" { attrs = append(attrs, slog.String("token", ale.Token)) } - if ale.DescopeError != "" { - attrs = append(attrs, slog.String("descope_error", ale.DescopeError)) + if ale.ProviderError != "" { + attrs = append(attrs, slog.String("provider_error", ale.ProviderError)) } - if ale.DescopeStatus != 0 { - attrs = append(attrs, slog.Int("descope_http_status", ale.DescopeStatus)) + if ale.ProviderStatus != 0 { + attrs = append(attrs, slog.Int("provider_http_status", ale.ProviderStatus)) } - if ale.DescopeBody != "" { - attrs = append(attrs, slog.String("descope_response_body", ale.DescopeBody)) + if ale.ProviderBody != "" { + attrs = append(attrs, slog.String("provider_response_body", ale.ProviderBody)) } if ale.RefreshToken != "" { attrs = append(attrs, slog.String("refresh_token", ale.RefreshToken)) diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index 65849c4f..2a60ea7b 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'auth_token_store.dart'; import 'http_client.dart'; import 'web_window.dart'; @@ -265,12 +266,17 @@ class AuthProxyService { if (token != null && token.isNotEmpty) { headers['Authorization'] = 'Bearer $token'; } + final sessionId = _extractSessionIdFromJwt(token ?? AuthTokenStore.getToken() ?? ''); final client = createHttpClient(withCredentials: true); try { final response = await client.post( url, headers: headers, - body: jsonEncode({'login_challenge': loginChallenge}), + body: jsonEncode({ + 'login_challenge': loginChallenge, + if (sessionId != null && sessionId.isNotEmpty) + 'approved_session_id': sessionId, + }), ); if (response.statusCode == 200) { @@ -284,6 +290,36 @@ class AuthProxyService { } } + static String? _extractSessionIdFromJwt(String token) { + if (token.isEmpty) { + return null; + } + try { + final parts = token.split('.'); + if (parts.length != 3) { + return null; + } + final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))); + final data = json.decode(payload) as Map; + for (final key in ['sid', 'session_id', 'sessionId', 'jti']) { + final value = data[key]; + if (value == null) { + continue; + } + if (value is String && value.isNotEmpty) { + return value; + } + final converted = value.toString(); + if (converted.isNotEmpty) { + return converted; + } + } + } catch (_) { + return null; + } + return null; + } + static Future> initiatePasswordReset(String loginId, {bool? drySend}) async { final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate'); final response = await http.post( diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index d2d44420..54a07942 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -571,14 +571,10 @@ class _DashboardScreenState extends ConsumerState { Widget _buildAppCell(AuditLogEntry log, {TextStyle? style}) { final label = _appLabelForLog(log); - if (label == 'Baron 통합로그인') { - return _selectableText(label, style: style); - } - final tooltip = log.parentSessionId.isEmpty - ? '부모 세션 ID 없음' - : '부모 세션 ID: ${log.parentSessionId}'; + final clientId = log.clientId; + final tooltip = clientId.isEmpty ? 'Client ID 없음' : 'Client ID: $clientId'; final baseStyle = style ?? const TextStyle(); - final emphasisStyle = log.parentSessionId.isEmpty + final emphasisStyle = clientId.isEmpty ? baseStyle : baseStyle.copyWith( color: Colors.blueAccent,