mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
feat(iflow): add cookie-based authentication endpoint
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
38
internal/auth/iflow/cookie_helpers.go
Normal file
38
internal/auth/iflow/cookie_helpers.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user