diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 68f71515..677f6b71 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -1443,6 +1443,87 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "url": authURL, "state": state}) } +func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { + ctx := context.Background() + + var payload struct { + Cookie string `json:"cookie"` + } + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "cookie is required"}) + return + } + + cookieValue := strings.TrimSpace(payload.Cookie) + + if cookieValue == "" { + c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "cookie is required"}) + return + } + + cookieValue, errNormalize := iflowauth.NormalizeCookie(cookieValue) + if errNormalize != nil { + c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": errNormalize.Error()}) + return + } + + authSvc := iflowauth.NewIFlowAuth(h.cfg) + tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue) + if errAuth != nil { + c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": errAuth.Error()}) + return + } + + tokenData.Cookie = cookieValue + + tokenStorage := authSvc.CreateCookieTokenStorage(tokenData) + email := strings.TrimSpace(tokenStorage.Email) + if email == "" { + c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "failed to extract email from token"}) + return + } + + fileName := iflowauth.SanitizeIFlowFileName(email) + if fileName == "" { + fileName = fmt.Sprintf("iflow-%d", time.Now().UnixMilli()) + } + + tokenStorage.Email = email + + record := &coreauth.Auth{ + ID: fmt.Sprintf("iflow-%s.json", fileName), + Provider: "iflow", + FileName: fmt.Sprintf("iflow-%s.json", fileName), + Storage: tokenStorage, + Metadata: map[string]any{ + "email": email, + "api_key": tokenStorage.APIKey, + "expired": tokenStorage.Expire, + "cookie": tokenStorage.Cookie, + "type": tokenStorage.Type, + "last_refresh": tokenStorage.LastRefresh, + }, + Attributes: map[string]string{ + "api_key": tokenStorage.APIKey, + }, + } + + savedPath, errSave := h.saveTokenRecord(ctx, record) + if errSave != nil { + c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to save authentication tokens"}) + return + } + + fmt.Printf("iFlow cookie authentication successful. Token saved to %s\n", savedPath) + c.JSON(http.StatusOK, gin.H{ + "status": "ok", + "saved_path": savedPath, + "email": email, + "expired": tokenStorage.Expire, + "type": tokenStorage.Type, + }) +} + type projectSelectionRequiredError struct{} func (e *projectSelectionRequiredError) Error() string { diff --git a/internal/api/server.go b/internal/api/server.go index 78672f02..5c012c50 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -518,6 +518,7 @@ func (s *Server) registerManagementRoutes() { mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken) mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken) mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken) + mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken) mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) } } diff --git a/internal/auth/iflow/cookie_helpers.go b/internal/auth/iflow/cookie_helpers.go new file mode 100644 index 00000000..6848f4b0 --- /dev/null +++ b/internal/auth/iflow/cookie_helpers.go @@ -0,0 +1,38 @@ +package iflow + +import ( + "fmt" + "strings" +) + +// NormalizeCookie normalizes raw cookie strings for iFlow authentication flows. +func NormalizeCookie(raw string) (string, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "", fmt.Errorf("cookie cannot be empty") + } + + combined := strings.Join(strings.Fields(trimmed), " ") + if !strings.HasSuffix(combined, ";") { + combined += ";" + } + if !strings.Contains(combined, "BXAuth=") { + return "", fmt.Errorf("cookie missing BXAuth field") + } + return combined, nil +} + +// SanitizeIFlowFileName normalizes user identifiers for safe filename usage. +func SanitizeIFlowFileName(raw string) string { + if raw == "" { + return "" + } + cleanEmail := strings.ReplaceAll(raw, "*", "x") + var result strings.Builder + for _, r := range cleanEmail { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '@' || r == '.' || r == '-' { + result.WriteRune(r) + } + } + return strings.TrimSpace(result.String()) +} diff --git a/internal/cmd/iflow_cookie.go b/internal/cmd/iflow_cookie.go index 9d99a5bd..b1cb1f9c 100644 --- a/internal/cmd/iflow_cookie.go +++ b/internal/cmd/iflow_cookie.go @@ -71,22 +71,9 @@ func promptForCookie(promptFn func(string) (string, error)) (string, error) { return "", fmt.Errorf("failed to read cookie: %w", err) } - line = strings.TrimSpace(line) - if line == "" { - return "", fmt.Errorf("cookie cannot be empty") - } - - // Clean up any extra whitespace and join multiple spaces - cookie := strings.Join(strings.Fields(line), " ") - - // Ensure it ends properly - if !strings.HasSuffix(cookie, ";") { - cookie = cookie + ";" - } - - // Ensure BXAuth is present in the cookie - if !strings.Contains(cookie, "BXAuth=") { - return "", fmt.Errorf("BXAuth field not found in cookie") + cookie, err := iflow.NormalizeCookie(line) + if err != nil { + return "", err } return cookie, nil @@ -94,17 +81,6 @@ func promptForCookie(promptFn func(string) (string, error)) (string, error) { // getAuthFilePath returns the auth file path for the given provider and email func getAuthFilePath(cfg *config.Config, provider, email string) string { - // Clean email to make it filename-safe - cleanEmail := strings.ReplaceAll(email, "*", "x") - - // Remove any unsafe characters, but allow standard email chars (@, ., -) - var result strings.Builder - for _, r := range cleanEmail { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || - r == '_' || r == '@' || r == '.' || r == '-' { - result.WriteRune(r) - } - } - - return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, result.String()) + fileName := iflow.SanitizeIFlowFileName(email) + return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, fileName) }