package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "context" crand "crypto/rand" "encoding/hex" "encoding/json" "fmt" "log" "math/rand" "os" "strings" "time" "github.com/descope/go-sdk/descope" "github.com/descope/go-sdk/descope/client" "github.com/gofiber/fiber/v2" ) type AuthHandler struct { ProjectID string SmsService domain.SmsService RedisService *service.RedisService DescopeClient *client.DescopeClient } // Helper to generate secure random strings func generateSecureToken(length int) string { b := make([]byte, length) if _, err := crand.Read(b); err != nil { return "" } return hex.EncodeToString(b) } func NewAuthHandler() *AuthHandler { redisService, err := service.NewRedisService() if err != nil { log.Fatalf("Failed to connect to Redis: %v", err) } projectID := os.Getenv("DESCOPE_PROJECT_ID") managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY") var descopeClient *client.DescopeClient if projectID != "" { descopeClient, err = client.NewWithConfig(&client.Config{ ProjectID: projectID, ManagementKey: managementKey, }) if err != nil { log.Printf("Warning: Failed to initialize Descope Client: %v", err) } } return &AuthHandler{ ProjectID: projectID, SmsService: service.NewSmsService(), RedisService: redisService, DescopeClient: descopeClient, } } // SendSms sends a verification code via SMS. (Restored for completeness) func (h *AuthHandler) SendSms(c *fiber.Ctx) error { var req domain.SmsRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } log.Printf("[SMS] Sending code to: %s", req.PhoneNumber) sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") rand.Seed(time.Now().UnixNano()) code := fmt.Sprintf("%06d", rand.Intn(1000000)) content := fmt.Sprintf("[Baron SSO] 인증번호: %s", code) h.RedisService.StoreVerificationCode(sanitizedPhone, code) if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) } return c.JSON(fiber.Map{"message": "SMS sent successfully"}) } // VerifySms verifies the provided SMS code. (Restored) func (h *AuthHandler) VerifySms(c *fiber.Ctx) error { var req domain.SmsVerifyRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") storedCode, _ := h.RedisService.GetVerificationCode(sanitizedPhone) if storedCode == "" || storedCode != req.Code { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"}) } h.RedisService.DeleteVerificationCode(sanitizedPhone) // Note: In a real scenario, you might want to generate a Descope JWT here too // using the same logic as VerifyMagicLink, but for now returning a placeholder // or you can call the Descope logic if needed. token := "sms-verified-placeholder-token" return c.JSON(fiber.Map{"token": token}) } // InitEnchantedLink - Custom Implementation (Restored) func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { var req domain.EnchantedLinkInitRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } loginID := strings.ReplaceAll(req.LoginID, "-", "") loginID = strings.ReplaceAll(loginID, " ", "") // Generate secure tokens token := generateSecureToken(4) pendingRef := generateSecureToken(4) // Store in Redis h.RedisService.Set("enchanted_session:"+pendingRef, `{"status":"pending"}`, 5*time.Minute) h.RedisService.Set("enchanted_token:"+token, fmt.Sprintf(`{"pendingRef":"%s","loginId":"%s"}`, pendingRef, loginID), 5*time.Minute) // Send SMS // Frontend URL should be dynamic or env based, but restoring hardcoded/env logic // The frontend uses ssologin.hmac.kr frontendURL := "http://ssologin.hmac.kr" link := fmt.Sprintf("%s/?t=%s", frontendURL, token) content := fmt.Sprintf("[Baron SSO] 로그인 링크: %s", link) log.Printf("[Enchanted] Sending link to %s", loginID) if err := h.SmsService.SendSms(loginID, content); err != nil { log.Printf("[Enchanted] SMS Failed: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) } return c.JSON(fiber.Map{ "linkId": "SMS Sent", "pendingRef": pendingRef, "maskedEmail": loginID, }) } // PollEnchantedLink - Check status (Restored) func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error { var req domain.EnchantedLinkPollRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } val, err := h.RedisService.Get("enchanted_session:" + req.PendingRef) if err != nil || val == "" { return c.JSON(fiber.Map{"status": "pending"}) } var data map[string]string json.Unmarshal([]byte(val), &data) if data["status"] == "success" { return c.JSON(fiber.Map{ "sessionJwt": data["jwt"], "status": "ok", }) } return c.JSON(fiber.Map{"status": "pending"}) } // VerifyMagicLink - Validate token and login (Restored) func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { var req domain.MagicLinkVerifyRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } tokenKey := "enchanted_token:" + req.Token val, err := h.RedisService.Get(tokenKey) if err != nil || val == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired token"}) } var tokenData map[string]string json.Unmarshal([]byte(val), &data := tokenData) pendingRef := tokenData["pendingRef"] loginID := tokenData["loginId"] // 1. Generate Descope Session Directly (Management SDK) if h.DescopeClient == nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"}) } // Use GenerateEmbeddedLink to get a temporary token directly for the user. // This generates a token that will be exchanged for a real session. embeddedToken, err := h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), loginID, nil, 0) if err != nil { // If user does not exist, create it and retry if strings.Contains(err.Error(), "User not found") || strings.Contains(err.Error(), "E062108") { log.Printf("User %s not found. Creating new user...", loginID) // Format LoginID for Descope (E.164 for phones) descopeLoginID := loginID userObj := &descope.UserRequest{} if strings.Contains(loginID, "@") { userObj.Email = loginID } else { // LoginID is likely a phone number if strings.HasPrefix(loginID, "010") { descopeLoginID = "+82" + loginID[1:] } userObj.Phone = descopeLoginID } // Create user using the formatted LoginID _, errCreate := h.DescopeClient.Management.User().Create(context.Background(), descopeLoginID, userObj) if errCreate != nil { log.Printf("Failed to create user: %v", errCreate) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create new user"}) } // Retry generating embedded token with the Descope LoginID embeddedToken, err = h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), descopeLoginID, nil, 0) if err != nil { log.Printf("Failed to generate Descope Session after creation: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate token for new user"}) } } else { log.Printf("Failed to generate Descope Session: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate upstream token"}) } } // Exchange the Embedded Token for a real User Session JWT authInfo, err := h.DescopeClient.Auth.MagicLink().Verify(context.Background(), embeddedToken, nil) if err != nil { log.Printf("Failed to verify embedded token: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify upstream token"}) } sessionToken := authInfo.SessionToken.JWT // Update Session in Redis for the polling client sessionData, _ := json.Marshal(map[string]string{ "status": "success", "jwt": sessionToken, }) h.RedisService.Set("enchanted_session:"+pendingRef, string(sessionData), 5*time.Minute) return c.JSON(fiber.Map{ "token": sessionToken, "message": "Login successful", }) } // ProxyToDescope (Placeholder) func (h *AuthHandler) ProxyToDescope(c *fiber.Ctx, path string, payload interface{}) error { return c.Status(501).SendString("Descope Proxy Disabled") } // 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 { log.Printf("[Webhook] Body parsing failed: %v", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } if req.Recipient == "" || req.Body == "" { log.Printf("[Webhook] Missing recipient or body") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing recipient or body"}) } log.Printf("[Webhook] Received SMS request for %s", 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 { log.Printf("[Webhook] Failed to forward SMS to Naver: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS via Naver"}) } log.Printf("[Webhook] Successfully forwarded SMS to %s", 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 { log.Printf("[Email Webhook] Body parsing failed: %v", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } log.Printf("[Email Webhook] Received email request for %s", 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 { log.Printf("[Email Webhook] Failed to forward Email-as-SMS: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) } log.Printf("[Email Webhook] Successfully converted Email to SMS for %s", 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. log.Printf("[Email Webhook] Real email skipped (Not implemented): %s", req.To) return c.Status(501).JSON(fiber.Map{"error": "Real email sending not implemented"}) }