mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
Merge pull request #270 from router-for-me/iflow
feat(auth): add iFlow cookie-based authentication support
This commit is contained in:
@@ -59,6 +59,7 @@ func main() {
|
|||||||
var claudeLogin bool
|
var claudeLogin bool
|
||||||
var qwenLogin bool
|
var qwenLogin bool
|
||||||
var iflowLogin bool
|
var iflowLogin bool
|
||||||
|
var iflowCookie bool
|
||||||
var noBrowser bool
|
var noBrowser bool
|
||||||
var projectID string
|
var projectID string
|
||||||
var vertexImport string
|
var vertexImport string
|
||||||
@@ -71,6 +72,7 @@ func main() {
|
|||||||
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
||||||
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
||||||
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
||||||
|
flag.BoolVar(&iflowCookie, "iflow-cookie", false, "Login to iFlow using Cookie")
|
||||||
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
||||||
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
||||||
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
|
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
|
||||||
@@ -439,6 +441,8 @@ func main() {
|
|||||||
cmd.DoQwenLogin(cfg, options)
|
cmd.DoQwenLogin(cfg, options)
|
||||||
} else if iflowLogin {
|
} else if iflowLogin {
|
||||||
cmd.DoIFlowLogin(cfg, options)
|
cmd.DoIFlowLogin(cfg, options)
|
||||||
|
} else if iflowCookie {
|
||||||
|
cmd.DoIFlowCookieAuth(cfg, options)
|
||||||
} else {
|
} else {
|
||||||
// In cloud deploy mode without config file, just wait for shutdown signals
|
// In cloud deploy mode without config file, just wait for shutdown signals
|
||||||
if isCloudDeploy && !configFileExists {
|
if isCloudDeploy && !configFileExists {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package iflow
|
package iflow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -23,6 +24,9 @@ const (
|
|||||||
iFlowUserInfoEndpoint = "https://iflow.cn/api/oauth/getUserInfo"
|
iFlowUserInfoEndpoint = "https://iflow.cn/api/oauth/getUserInfo"
|
||||||
iFlowSuccessRedirectURL = "https://iflow.cn/oauth/success"
|
iFlowSuccessRedirectURL = "https://iflow.cn/oauth/success"
|
||||||
|
|
||||||
|
// Cookie authentication endpoints
|
||||||
|
iFlowAPIKeyEndpoint = "https://platform.iflow.cn/api/openapi/apikey"
|
||||||
|
|
||||||
// Client credentials provided by iFlow for the Code Assist integration.
|
// Client credentials provided by iFlow for the Code Assist integration.
|
||||||
iFlowOAuthClientID = "10009311001"
|
iFlowOAuthClientID = "10009311001"
|
||||||
iFlowOAuthClientSecret = "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW"
|
iFlowOAuthClientSecret = "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW"
|
||||||
@@ -261,6 +265,7 @@ type IFlowTokenData struct {
|
|||||||
Expire string
|
Expire string
|
||||||
APIKey string
|
APIKey string
|
||||||
Email string
|
Email string
|
||||||
|
Cookie string
|
||||||
}
|
}
|
||||||
|
|
||||||
// userInfoResponse represents the structure returned by the user info endpoint.
|
// userInfoResponse represents the structure returned by the user info endpoint.
|
||||||
@@ -274,3 +279,232 @@ type userInfoData struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// iFlowAPIKeyResponse represents the response from the API key endpoint
|
||||||
|
type iFlowAPIKeyResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data iFlowKeyData `json:"data"`
|
||||||
|
Extra interface{} `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// iFlowKeyData contains the API key information
|
||||||
|
type iFlowKeyData struct {
|
||||||
|
HasExpired bool `json:"hasExpired"`
|
||||||
|
ExpireTime string `json:"expireTime"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
APIKeyMask string `json:"apiKeyMask"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// iFlowRefreshRequest represents the request body for refreshing API key
|
||||||
|
type iFlowRefreshRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateWithCookie performs authentication using browser cookies
|
||||||
|
func (ia *IFlowAuth) AuthenticateWithCookie(ctx context.Context, cookie string) (*IFlowTokenData, error) {
|
||||||
|
if strings.TrimSpace(cookie) == "" {
|
||||||
|
return nil, fmt.Errorf("iflow cookie authentication: cookie is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, get initial API key information using GET request
|
||||||
|
keyInfo, err := ia.fetchAPIKeyInfo(ctx, cookie)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie authentication: fetch initial API key info failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to token data format
|
||||||
|
data := &IFlowTokenData{
|
||||||
|
APIKey: keyInfo.APIKey,
|
||||||
|
Expire: keyInfo.ExpireTime,
|
||||||
|
Email: keyInfo.Name,
|
||||||
|
Cookie: cookie,
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchAPIKeyInfo retrieves API key information using GET request with cookie
|
||||||
|
func (ia *IFlowAuth) fetchAPIKeyInfo(ctx context.Context, cookie string) (*iFlowKeyData, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, iFlowAPIKeyEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie: create GET request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cookie and other headers to mimic browser
|
||||||
|
req.Header.Set("Cookie", cookie)
|
||||||
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||||
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
|
req.Header.Set("Connection", "keep-alive")
|
||||||
|
req.Header.Set("Sec-Fetch-Dest", "empty")
|
||||||
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||||
|
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
||||||
|
|
||||||
|
resp, err := ia.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie: GET request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
// Handle gzip compression
|
||||||
|
var reader io.Reader = resp.Body
|
||||||
|
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
gzipReader, err := gzip.NewReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie: create gzip reader failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = gzipReader.Close() }()
|
||||||
|
reader = gzipReader
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie: read GET response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Debugf("iflow cookie GET request failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||||
|
return nil, fmt.Errorf("iflow cookie: GET request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyResp iFlowAPIKeyResponse
|
||||||
|
if err = json.Unmarshal(body, &keyResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie: decode GET response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keyResp.Success {
|
||||||
|
return nil, fmt.Errorf("iflow cookie: GET request not successful: %s", keyResp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle initial response where apiKey field might be apiKeyMask
|
||||||
|
if keyResp.Data.APIKey == "" && keyResp.Data.APIKeyMask != "" {
|
||||||
|
keyResp.Data.APIKey = keyResp.Data.APIKeyMask
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keyResp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshAPIKey refreshes the API key using POST request
|
||||||
|
func (ia *IFlowAuth) RefreshAPIKey(ctx context.Context, cookie, name string) (*iFlowKeyData, error) {
|
||||||
|
if strings.TrimSpace(cookie) == "" {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: cookie is empty")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare request body
|
||||||
|
refreshReq := iFlowRefreshRequest{
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(refreshReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: marshal request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, iFlowAPIKeyEndpoint, strings.NewReader(string(bodyBytes)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: create POST request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cookie and other headers to mimic browser
|
||||||
|
req.Header.Set("Cookie", cookie)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||||
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
|
req.Header.Set("Connection", "keep-alive")
|
||||||
|
req.Header.Set("Origin", "https://platform.iflow.cn")
|
||||||
|
req.Header.Set("Referer", "https://platform.iflow.cn/")
|
||||||
|
|
||||||
|
resp, err := ia.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: POST request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
// Handle gzip compression
|
||||||
|
var reader io.Reader = resp.Body
|
||||||
|
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
gzipReader, err := gzip.NewReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: create gzip reader failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = gzipReader.Close() }()
|
||||||
|
reader = gzipReader
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: read POST response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Debugf("iflow cookie POST request failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: POST request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyResp iFlowAPIKeyResponse
|
||||||
|
if err = json.Unmarshal(body, &keyResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: decode POST response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keyResp.Success {
|
||||||
|
return nil, fmt.Errorf("iflow cookie refresh: POST request not successful: %s", keyResp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keyResp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRefreshAPIKey checks if the API key needs to be refreshed (within 2 days of expiry)
|
||||||
|
func ShouldRefreshAPIKey(expireTime string) (bool, time.Duration, error) {
|
||||||
|
if strings.TrimSpace(expireTime) == "" {
|
||||||
|
return false, 0, fmt.Errorf("iflow cookie: expire time is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
expire, err := time.Parse("2006-01-02 15:04", expireTime)
|
||||||
|
if err != nil {
|
||||||
|
return false, 0, fmt.Errorf("iflow cookie: parse expire time failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
twoDaysFromNow := now.Add(48 * time.Hour)
|
||||||
|
|
||||||
|
needsRefresh := expire.Before(twoDaysFromNow)
|
||||||
|
timeUntilExpiry := expire.Sub(now)
|
||||||
|
|
||||||
|
return needsRefresh, timeUntilExpiry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCookieTokenStorage converts cookie-based token data into persistence storage
|
||||||
|
func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenStorage {
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &IFlowTokenStorage{
|
||||||
|
APIKey: data.APIKey,
|
||||||
|
Email: data.Email,
|
||||||
|
Expire: data.Expire,
|
||||||
|
Cookie: data.Cookie,
|
||||||
|
LastRefresh: time.Now().Format(time.RFC3339),
|
||||||
|
Type: "iflow",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCookieTokenStorage updates the persisted token storage with refreshed API key data
|
||||||
|
func (ia *IFlowAuth) UpdateCookieTokenStorage(storage *IFlowTokenStorage, keyData *iFlowKeyData) {
|
||||||
|
if storage == nil || keyData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.APIKey = keyData.APIKey
|
||||||
|
storage.Expire = keyData.ExpireTime
|
||||||
|
storage.LastRefresh = time.Now().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type IFlowTokenStorage struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
|
Cookie string `json:"cookie"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
111
internal/cmd/iflow_cookie.go
Normal file
111
internal/cmd/iflow_cookie.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoIFlowCookieAuth performs the iFlow cookie-based authentication.
|
||||||
|
func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) {
|
||||||
|
if options == nil {
|
||||||
|
options = &LoginOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
promptFn = func(prompt string) (string, error) {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
value, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(value), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt user for cookie
|
||||||
|
cookie, err := promptForCookie(promptFn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to get cookie: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate with cookie
|
||||||
|
auth := iflow.NewIFlowAuth(cfg)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tokenData, err := auth.AuthenticateWithCookie(ctx, cookie)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("iFlow cookie authentication failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token storage
|
||||||
|
tokenStorage := auth.CreateCookieTokenStorage(tokenData)
|
||||||
|
|
||||||
|
// Get auth file path using email in filename
|
||||||
|
authFilePath := getAuthFilePath(cfg, "iflow", tokenData.Email)
|
||||||
|
|
||||||
|
// Save token to file
|
||||||
|
if err := tokenStorage.SaveTokenToFile(authFilePath); err != nil {
|
||||||
|
fmt.Printf("Failed to save authentication: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authentication successful! API key: %s\n", tokenData.APIKey)
|
||||||
|
fmt.Printf("Expires at: %s\n", tokenData.Expire)
|
||||||
|
fmt.Printf("Authentication saved to: %s\n", authFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptForCookie prompts the user to enter their iFlow cookie
|
||||||
|
func promptForCookie(promptFn func(string) (string, error)) (string, error) {
|
||||||
|
line, err := promptFn("Enter iFlow Cookie (from browser cookies): ")
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "@", "_at_")
|
||||||
|
cleanEmail = strings.ReplaceAll(cleanEmail, ".", "_")
|
||||||
|
cleanEmail = strings.ReplaceAll(cleanEmail, "-", "_")
|
||||||
|
|
||||||
|
// Remove any remaining special characters
|
||||||
|
var result strings.Builder
|
||||||
|
for _, r := range cleanEmail {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
||||||
|
result.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, result.String())
|
||||||
|
}
|
||||||
@@ -242,13 +242,87 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh refreshes OAuth tokens and updates the stored API key.
|
// Refresh refreshes OAuth tokens or cookie-based API keys and updates the stored API key.
|
||||||
func (e *IFlowExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
func (e *IFlowExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||||
log.Debugf("iflow executor: refresh called")
|
log.Debugf("iflow executor: refresh called")
|
||||||
if auth == nil {
|
if auth == nil {
|
||||||
return nil, fmt.Errorf("iflow executor: auth is nil")
|
return nil, fmt.Errorf("iflow executor: auth is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is cookie-based authentication
|
||||||
|
var cookie string
|
||||||
|
var email string
|
||||||
|
if auth.Metadata != nil {
|
||||||
|
if v, ok := auth.Metadata["cookie"].(string); ok {
|
||||||
|
cookie = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
if v, ok := auth.Metadata["email"].(string); ok {
|
||||||
|
email = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cookie is present, use cookie-based refresh
|
||||||
|
if cookie != "" && email != "" {
|
||||||
|
return e.refreshCookieBased(ctx, auth, cookie, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use OAuth-based refresh
|
||||||
|
return e.refreshOAuthBased(ctx, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshCookieBased refreshes API key using browser cookie
|
||||||
|
func (e *IFlowExecutor) refreshCookieBased(ctx context.Context, auth *cliproxyauth.Auth, cookie, email string) (*cliproxyauth.Auth, error) {
|
||||||
|
log.Debugf("iflow executor: checking refresh need for cookie-based API key for user: %s", email)
|
||||||
|
|
||||||
|
// Get current expiry time from metadata
|
||||||
|
var currentExpire string
|
||||||
|
if auth.Metadata != nil {
|
||||||
|
if v, ok := auth.Metadata["expired"].(string); ok {
|
||||||
|
currentExpire = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if refresh is needed
|
||||||
|
needsRefresh, _, err := iflowauth.ShouldRefreshAPIKey(currentExpire)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("iflow executor: failed to check refresh need: %v", err)
|
||||||
|
// If we can't check, continue with refresh anyway as a safety measure
|
||||||
|
} else if !needsRefresh {
|
||||||
|
log.Debugf("iflow executor: no refresh needed for user: %s", email)
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("iflow executor: refreshing cookie-based API key for user: %s", email)
|
||||||
|
|
||||||
|
svc := iflowauth.NewIFlowAuth(e.cfg)
|
||||||
|
keyData, err := svc.RefreshAPIKey(ctx, cookie, email)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("iflow executor: cookie-based API key refresh failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.Metadata == nil {
|
||||||
|
auth.Metadata = make(map[string]any)
|
||||||
|
}
|
||||||
|
auth.Metadata["api_key"] = keyData.APIKey
|
||||||
|
auth.Metadata["expired"] = keyData.ExpireTime
|
||||||
|
auth.Metadata["type"] = "iflow"
|
||||||
|
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
|
||||||
|
auth.Metadata["cookie"] = cookie
|
||||||
|
auth.Metadata["email"] = email
|
||||||
|
|
||||||
|
log.Infof("iflow executor: cookie-based API key refreshed successfully, new expiry: %s", keyData.ExpireTime)
|
||||||
|
|
||||||
|
if auth.Attributes == nil {
|
||||||
|
auth.Attributes = make(map[string]string)
|
||||||
|
}
|
||||||
|
auth.Attributes["api_key"] = keyData.APIKey
|
||||||
|
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshOAuthBased refreshes tokens using OAuth refresh token
|
||||||
|
func (e *IFlowExecutor) refreshOAuthBased(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||||
refreshToken := ""
|
refreshToken := ""
|
||||||
oldAccessToken := ""
|
oldAccessToken := ""
|
||||||
if auth.Metadata != nil {
|
if auth.Metadata != nil {
|
||||||
|
|||||||
@@ -152,11 +152,29 @@ func (a *Auth) AccountInfo() (string, string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.Metadata != nil {
|
|
||||||
if v, ok := a.Metadata["email"].(string); ok {
|
// For iFlow provider, prioritize OAuth type if email is present
|
||||||
return "oauth", v
|
if strings.ToLower(a.Provider) == "iflow" {
|
||||||
|
if a.Metadata != nil {
|
||||||
|
if email, ok := a.Metadata["email"].(string); ok {
|
||||||
|
email = strings.TrimSpace(email)
|
||||||
|
if email != "" {
|
||||||
|
return "oauth", email
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check metadata for email first (OAuth-style auth)
|
||||||
|
if a.Metadata != nil {
|
||||||
|
if v, ok := a.Metadata["email"].(string); ok {
|
||||||
|
email := strings.TrimSpace(v)
|
||||||
|
if email != "" {
|
||||||
|
return "oauth", email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to API key (API-key auth)
|
||||||
if a.Attributes != nil {
|
if a.Attributes != nil {
|
||||||
if v := a.Attributes["api_key"]; v != "" {
|
if v := a.Attributes["api_key"]; v != "" {
|
||||||
return "api_key", v
|
return "api_key", v
|
||||||
@@ -259,6 +277,7 @@ func parseTimeValue(v any) (time.Time, bool) {
|
|||||||
time.RFC3339,
|
time.RFC3339,
|
||||||
time.RFC3339Nano,
|
time.RFC3339Nano,
|
||||||
"2006-01-02 15:04:05",
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02 15:04",
|
||||||
"2006-01-02T15:04:05Z07:00",
|
"2006-01-02T15:04:05Z07:00",
|
||||||
}
|
}
|
||||||
for _, layout := range layouts {
|
for _, layout := range layouts {
|
||||||
|
|||||||
Reference in New Issue
Block a user