feat(iflow): add cookie-based authentication endpoint

This commit is contained in:
hkfires
2025-11-20 18:20:23 +08:00
parent 371324c090
commit 3f4f8b3b2d
4 changed files with 125 additions and 29 deletions

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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())
}

View File

@@ -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)
}