Merge pull request #502 from router-for-me/iflow

fix(auth): prevent duplicate iflow BXAuth tokens
This commit is contained in:
Luis Pater
2025-12-12 20:03:37 +08:00
committed by GitHub
5 changed files with 97 additions and 5 deletions

View File

@@ -1722,6 +1722,17 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
return 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) authSvc := iflowauth.NewIFlowAuth(h.cfg)
tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue) tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue)
if errAuth != nil { if errAuth != nil {
@@ -1744,11 +1755,12 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
} }
tokenStorage.Email = email tokenStorage.Email = email
timestamp := time.Now().Unix()
record := &coreauth.Auth{ record := &coreauth.Auth{
ID: fmt.Sprintf("iflow-%s.json", fileName), ID: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
Provider: "iflow", Provider: "iflow",
FileName: fmt.Sprintf("iflow-%s.json", fileName), FileName: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
Storage: tokenStorage, Storage: tokenStorage,
Metadata: map[string]any{ Metadata: map[string]any{
"email": email, "email": email,

View File

@@ -1,7 +1,10 @@
package iflow package iflow
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
) )
@@ -36,3 +39,61 @@ func SanitizeIFlowFileName(raw string) string {
} }
return strings.TrimSpace(result.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
}

View File

@@ -494,11 +494,18 @@ func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenS
return nil return nil
} }
// Only save the BXAuth field from the cookie
bxAuth := ExtractBXAuth(data.Cookie)
cookieToSave := ""
if bxAuth != "" {
cookieToSave = "BXAuth=" + bxAuth + ";"
}
return &IFlowTokenStorage{ return &IFlowTokenStorage{
APIKey: data.APIKey, APIKey: data.APIKey,
Email: data.Email, Email: data.Email,
Expire: data.Expire, Expire: data.Expire,
Cookie: data.Cookie, Cookie: cookieToSave,
LastRefresh: time.Now().Format(time.RFC3339), LastRefresh: time.Now().Format(time.RFC3339),
Type: "iflow", Type: "iflow",
} }

View File

@@ -5,7 +5,9 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
@@ -37,6 +39,16 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) {
return 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 // Authenticate with cookie
auth := iflow.NewIFlowAuth(cfg) auth := iflow.NewIFlowAuth(cfg)
ctx := context.Background() 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 // getAuthFilePath returns the auth file path for the given provider and email
func getAuthFilePath(cfg *config.Config, provider, email string) string { func getAuthFilePath(cfg *config.Config, provider, email string) string {
fileName := iflow.SanitizeIFlowFileName(email) 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())
} }

View File

@@ -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") 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{ metadata := map[string]any{
"email": email, "email": email,
"api_key": tokenStorage.APIKey, "api_key": tokenStorage.APIKey,