From e7cedbee6e980b22e03e0ea0deae7572d3ca3c28 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:57:19 +0800 Subject: [PATCH] fix(auth): prevent duplicate iflow BXAuth tokens --- .../api/handlers/management/auth_files.go | 16 ++++- internal/auth/iflow/cookie_helpers.go | 61 +++++++++++++++++++ internal/auth/iflow/iflow_auth.go | 9 ++- internal/cmd/iflow_cookie.go | 14 ++++- sdk/auth/iflow.go | 2 +- 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index e626af47..5909dffc 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -1722,6 +1722,17 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { return } + // Check for duplicate BXAuth before authentication + bxAuth := iflowauth.ExtractBXAuth(cookieValue) + if existingFile, err := iflowauth.CheckDuplicateBXAuth(h.cfg.AuthDir, bxAuth); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to check duplicate"}) + return + } else if existingFile != "" { + existingFileName := filepath.Base(existingFile) + c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "duplicate BXAuth found", "existing_file": existingFileName}) + return + } + authSvc := iflowauth.NewIFlowAuth(h.cfg) tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue) if errAuth != nil { @@ -1744,11 +1755,12 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { } tokenStorage.Email = email + timestamp := time.Now().Unix() record := &coreauth.Auth{ - ID: fmt.Sprintf("iflow-%s.json", fileName), + ID: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp), Provider: "iflow", - FileName: fmt.Sprintf("iflow-%s.json", fileName), + FileName: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp), Storage: tokenStorage, Metadata: map[string]any{ "email": email, diff --git a/internal/auth/iflow/cookie_helpers.go b/internal/auth/iflow/cookie_helpers.go index 6848f4b0..7e0f4264 100644 --- a/internal/auth/iflow/cookie_helpers.go +++ b/internal/auth/iflow/cookie_helpers.go @@ -1,7 +1,10 @@ package iflow import ( + "encoding/json" "fmt" + "os" + "path/filepath" "strings" ) @@ -36,3 +39,61 @@ func SanitizeIFlowFileName(raw string) string { } return strings.TrimSpace(result.String()) } + +// ExtractBXAuth extracts the BXAuth value from a cookie string. +func ExtractBXAuth(cookie string) string { + parts := strings.Split(cookie, ";") + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "BXAuth=") { + return strings.TrimPrefix(part, "BXAuth=") + } + } + return "" +} + +// CheckDuplicateBXAuth checks if the given BXAuth value already exists in any iflow auth file. +// Returns the path of the existing file if found, empty string otherwise. +func CheckDuplicateBXAuth(authDir, bxAuth string) (string, error) { + if bxAuth == "" { + return "", nil + } + + entries, err := os.ReadDir(authDir) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", fmt.Errorf("read auth dir failed: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasPrefix(name, "iflow-") || !strings.HasSuffix(name, ".json") { + continue + } + + filePath := filepath.Join(authDir, name) + data, err := os.ReadFile(filePath) + if err != nil { + continue + } + + var tokenData struct { + Cookie string `json:"cookie"` + } + if err := json.Unmarshal(data, &tokenData); err != nil { + continue + } + + existingBXAuth := ExtractBXAuth(tokenData.Cookie) + if existingBXAuth != "" && existingBXAuth == bxAuth { + return filePath, nil + } + } + + return "", nil +} diff --git a/internal/auth/iflow/iflow_auth.go b/internal/auth/iflow/iflow_auth.go index 2978e94c..fa9f38c3 100644 --- a/internal/auth/iflow/iflow_auth.go +++ b/internal/auth/iflow/iflow_auth.go @@ -494,11 +494,18 @@ func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenS return nil } + // Only save the BXAuth field from the cookie + bxAuth := ExtractBXAuth(data.Cookie) + cookieToSave := "" + if bxAuth != "" { + cookieToSave = "BXAuth=" + bxAuth + ";" + } + return &IFlowTokenStorage{ APIKey: data.APIKey, Email: data.Email, Expire: data.Expire, - Cookie: data.Cookie, + Cookie: cookieToSave, LastRefresh: time.Now().Format(time.RFC3339), Type: "iflow", } diff --git a/internal/cmd/iflow_cookie.go b/internal/cmd/iflow_cookie.go index b1cb1f9c..358b8062 100644 --- a/internal/cmd/iflow_cookie.go +++ b/internal/cmd/iflow_cookie.go @@ -5,7 +5,9 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" + "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -37,6 +39,16 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) { return } + // Check for duplicate BXAuth before authentication + bxAuth := iflow.ExtractBXAuth(cookie) + if existingFile, err := iflow.CheckDuplicateBXAuth(cfg.AuthDir, bxAuth); err != nil { + fmt.Printf("Failed to check duplicate: %v\n", err) + return + } else if existingFile != "" { + fmt.Printf("Duplicate BXAuth found, authentication already exists: %s\n", filepath.Base(existingFile)) + return + } + // Authenticate with cookie auth := iflow.NewIFlowAuth(cfg) ctx := context.Background() @@ -82,5 +94,5 @@ 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 { fileName := iflow.SanitizeIFlowFileName(email) - return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, fileName) + return fmt.Sprintf("%s/%s-%s-%d.json", cfg.AuthDir, provider, fileName, time.Now().Unix()) } diff --git a/sdk/auth/iflow.go b/sdk/auth/iflow.go index a240b431..ee96bdaa 100644 --- a/sdk/auth/iflow.go +++ b/sdk/auth/iflow.go @@ -107,7 +107,7 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts return nil, fmt.Errorf("iflow authentication failed: missing account identifier") } - fileName := fmt.Sprintf("iflow-%s.json", email) + fileName := fmt.Sprintf("iflow-%s-%d.json", email, time.Now().Unix()) metadata := map[string]any{ "email": email, "api_key": tokenStorage.APIKey,