mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +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})
|
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{}
|
type projectSelectionRequiredError struct{}
|
||||||
|
|
||||||
func (e *projectSelectionRequiredError) Error() string {
|
func (e *projectSelectionRequiredError) Error() string {
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
|
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
|
||||||
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
||||||
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
||||||
|
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
||||||
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
|
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)
|
return "", fmt.Errorf("failed to read cookie: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
line = strings.TrimSpace(line)
|
cookie, err := iflow.NormalizeCookie(line)
|
||||||
if line == "" {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cookie cannot be empty")
|
return "", err
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cookie, nil
|
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
|
// 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 {
|
||||||
// Clean email to make it filename-safe
|
fileName := iflow.SanitizeIFlowFileName(email)
|
||||||
cleanEmail := strings.ReplaceAll(email, "*", "x")
|
return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, fileName)
|
||||||
|
|
||||||
// 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())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user