package management import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/antigravity" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) func (h *Handler) RequestAnthropicToken(c *gin.Context) { ctx := context.Background() fmt.Println("Initializing Claude authentication...") // Generate PKCE codes pkceCodes, err := claude.GeneratePKCECodes() if err != nil { log.Errorf("Failed to generate PKCE codes: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate PKCE codes"}) return } // Generate random state parameter state, err := misc.GenerateRandomState() if err != nil { log.Errorf("Failed to generate state parameter: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate state parameter"}) return } // Initialize Claude auth service anthropicAuth := claude.NewClaudeAuth(h.cfg) // Generate authorization URL (then override redirect_uri to reuse server port) authURL, state, err := anthropicAuth.GenerateAuthURL(state, pkceCodes) if err != nil { log.Errorf("Failed to generate authorization URL: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization url"}) return } RegisterOAuthSession(state, "anthropic") isWebUI := isWebUIRequest(c) var forwarder *callbackForwarder if isWebUI { targetURL, errTarget := h.managementCallbackURL("/anthropic/callback") if errTarget != nil { log.WithError(errTarget).Error("failed to compute anthropic callback target") c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"}) return } var errStart error if forwarder, errStart = startCallbackForwarder(anthropicCallbackPort, "anthropic", targetURL); errStart != nil { log.WithError(errStart).Error("failed to start anthropic callback forwarder") c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"}) return } } go func() { if isWebUI { defer stopCallbackForwarderInstance(anthropicCallbackPort, forwarder) } // Helper: wait for callback file waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-anthropic-%s.oauth", state)) waitForFile := func(path string, timeout time.Duration) (map[string]string, error) { deadline := time.Now().Add(timeout) for { if !IsOAuthSessionPending(state, "anthropic") { return nil, errOAuthSessionNotPending } if time.Now().After(deadline) { SetOAuthSessionError(state, "Timeout waiting for OAuth callback") return nil, fmt.Errorf("timeout waiting for OAuth callback") } data, errRead := os.ReadFile(path) if errRead == nil { var m map[string]string _ = json.Unmarshal(data, &m) _ = os.Remove(path) return m, nil } time.Sleep(500 * time.Millisecond) } } fmt.Println("Waiting for authentication callback...") // Wait up to 5 minutes resultMap, errWait := waitForFile(waitFile, 5*time.Minute) if errWait != nil { if errors.Is(errWait, errOAuthSessionNotPending) { return } authErr := claude.NewAuthenticationError(claude.ErrCallbackTimeout, errWait) log.Error(claude.GetUserFriendlyMessage(authErr)) return } if errStr := resultMap["error"]; errStr != "" { oauthErr := claude.NewOAuthError(errStr, "", http.StatusBadRequest) log.Error(claude.GetUserFriendlyMessage(oauthErr)) SetOAuthSessionError(state, "Bad request") return } if resultMap["state"] != state { authErr := claude.NewAuthenticationError(claude.ErrInvalidState, fmt.Errorf("expected %s, got %s", state, resultMap["state"])) log.Error(claude.GetUserFriendlyMessage(authErr)) SetOAuthSessionError(state, "State code error") return } // Parse code (Claude may append state after '#') rawCode := resultMap["code"] code := strings.Split(rawCode, "#")[0] // Exchange code for tokens using internal auth service bundle, errExchange := anthropicAuth.ExchangeCodeForTokens(ctx, code, state, pkceCodes) if errExchange != nil { authErr := claude.NewAuthenticationError(claude.ErrCodeExchangeFailed, errExchange) log.Errorf("Failed to exchange authorization code for tokens: %v", authErr) SetOAuthSessionError(state, "Failed to exchange authorization code for tokens") return } // Create token storage tokenStorage := anthropicAuth.CreateTokenStorage(bundle) record := &coreauth.Auth{ ID: fmt.Sprintf("claude-%s.json", tokenStorage.Email), Provider: "claude", FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email), Storage: tokenStorage, Metadata: map[string]any{"email": tokenStorage.Email}, } savedPath, errSave := h.saveTokenRecord(ctx, record) if errSave != nil { log.Errorf("Failed to save authentication tokens: %v", errSave) SetOAuthSessionError(state, "Failed to save authentication tokens") return } fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) if bundle.APIKey != "" { fmt.Println("API key obtained and saved") } fmt.Println("You can now use Claude services through this CLI") CompleteOAuthSession(state) CompleteOAuthSessionsByProvider("anthropic") }() c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state}) } func (h *Handler) RequestGeminiCLIToken(c *gin.Context) { ctx := context.Background() proxyHTTPClient := util.SetProxy(&h.cfg.SDKConfig, &http.Client{}) ctx = context.WithValue(ctx, oauth2.HTTPClient, proxyHTTPClient) // Optional project ID from query projectID := c.Query("project_id") fmt.Println("Initializing Google authentication...") // OAuth2 configuration using exported constants from internal/auth/gemini conf := &oauth2.Config{ ClientID: geminiAuth.ClientID, ClientSecret: geminiAuth.ClientSecret, RedirectURL: fmt.Sprintf("http://localhost:%d/oauth2callback", geminiAuth.DefaultCallbackPort), Scopes: geminiAuth.Scopes, Endpoint: google.Endpoint, } // Build authorization URL and return it immediately state := fmt.Sprintf("gem-%d", time.Now().UnixNano()) authURL := conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")) RegisterOAuthSession(state, "gemini") isWebUI := isWebUIRequest(c) var forwarder *callbackForwarder if isWebUI { targetURL, errTarget := h.managementCallbackURL("/google/callback") if errTarget != nil { log.WithError(errTarget).Error("failed to compute gemini callback target") c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"}) return } var errStart error if forwarder, errStart = startCallbackForwarder(geminiCallbackPort, "gemini", targetURL); errStart != nil { log.WithError(errStart).Error("failed to start gemini callback forwarder") c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"}) return } } go func() { if isWebUI { defer stopCallbackForwarderInstance(geminiCallbackPort, forwarder) } // Wait for callback file written by server route waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-gemini-%s.oauth", state)) fmt.Println("Waiting for authentication callback...") deadline := time.Now().Add(5 * time.Minute) var authCode string for { if !IsOAuthSessionPending(state, "gemini") { return } if time.Now().After(deadline) { log.Error("oauth flow timed out") SetOAuthSessionError(state, "OAuth flow timed out") return } if data, errR := os.ReadFile(waitFile); errR == nil { var m map[string]string _ = json.Unmarshal(data, &m) _ = os.Remove(waitFile) if errStr := m["error"]; errStr != "" { log.Errorf("Authentication failed: %s", errStr) SetOAuthSessionError(state, "Authentication failed") return } authCode = m["code"] if authCode == "" { log.Errorf("Authentication failed: code not found") SetOAuthSessionError(state, "Authentication failed: code not found") return } break } time.Sleep(500 * time.Millisecond) } // Exchange authorization code for token token, err := conf.Exchange(ctx, authCode) if err != nil { log.Errorf("Failed to exchange token: %v", err) SetOAuthSessionError(state, "Failed to exchange token") return } requestedProjectID := strings.TrimSpace(projectID) // Create token storage (mirrors internal/auth/gemini createTokenStorage) authHTTPClient := conf.Client(ctx, token) req, errNewRequest := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", http.NoBody) if errNewRequest != nil { log.Errorf("Could not get user info: %v", errNewRequest) SetOAuthSessionError(state, "Could not get user info") return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) resp, errDo := authHTTPClient.Do(req) if errDo != nil { log.Errorf("Failed to execute request: %v", errDo) SetOAuthSessionError(state, "Failed to execute request") return } defer func() { if errClose := resp.Body.Close(); errClose != nil { log.Printf("warn: failed to close response body: %v", errClose) } }() bodyBytes, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { log.Errorf("Get user info request failed with status %d: %s", resp.StatusCode, string(bodyBytes)) SetOAuthSessionError(state, fmt.Sprintf("Get user info request failed with status %d", resp.StatusCode)) return } email := gjson.GetBytes(bodyBytes, "email").String() if email != "" { fmt.Printf("Authenticated user email: %s\n", email) } else { fmt.Println("Failed to get user email from token") } // Marshal/unmarshal oauth2.Token to generic map and enrich fields var ifToken map[string]any jsonData, _ := json.Marshal(token) if errUnmarshal := json.Unmarshal(jsonData, &ifToken); errUnmarshal != nil { log.Errorf("Failed to unmarshal token: %v", errUnmarshal) SetOAuthSessionError(state, "Failed to unmarshal token") return } ifToken["token_uri"] = "https://oauth2.googleapis.com/token" ifToken["client_id"] = geminiAuth.ClientID ifToken["client_secret"] = geminiAuth.ClientSecret ifToken["scopes"] = geminiAuth.Scopes ifToken["universe_domain"] = "googleapis.com" ts := geminiAuth.GeminiTokenStorage{ Token: ifToken, ProjectID: requestedProjectID, Email: email, Auto: requestedProjectID == "", } // Initialize authenticated HTTP client via GeminiAuth to honor proxy settings gemAuth := geminiAuth.NewGeminiAuth() gemClient, errGetClient := gemAuth.GetAuthenticatedClient(ctx, &ts, h.cfg, &geminiAuth.WebLoginOptions{ NoBrowser: true, }) if errGetClient != nil { log.Errorf("failed to get authenticated client: %v", errGetClient) SetOAuthSessionError(state, "Failed to get authenticated client") return } fmt.Println("Authentication successful.") if strings.EqualFold(requestedProjectID, "ALL") { ts.Auto = false projects, errAll := onboardAllGeminiProjects(ctx, gemClient, &ts) if errAll != nil { log.Errorf("Failed to complete Gemini CLI onboarding: %v", errAll) SetOAuthSessionError(state, "Failed to complete Gemini CLI onboarding") return } if errVerify := ensureGeminiProjectsEnabled(ctx, gemClient, projects); errVerify != nil { log.Errorf("Failed to verify Cloud AI API status: %v", errVerify) SetOAuthSessionError(state, "Failed to verify Cloud AI API status") return } ts.ProjectID = strings.Join(projects, ",") ts.Checked = true } else { if errEnsure := ensureGeminiProjectAndOnboard(ctx, gemClient, &ts, requestedProjectID); errEnsure != nil { log.Errorf("Failed to complete Gemini CLI onboarding: %v", errEnsure) SetOAuthSessionError(state, "Failed to complete Gemini CLI onboarding") return } if strings.TrimSpace(ts.ProjectID) == "" { log.Error("Onboarding did not return a project ID") SetOAuthSessionError(state, "Failed to resolve project ID") return } isChecked, errCheck := checkCloudAPIIsEnabled(ctx, gemClient, ts.ProjectID) if errCheck != nil { log.Errorf("Failed to verify Cloud AI API status: %v", errCheck) SetOAuthSessionError(state, "Failed to verify Cloud AI API status") return } ts.Checked = isChecked if !isChecked { log.Error("Cloud AI API is not enabled for the selected project") SetOAuthSessionError(state, "Cloud AI API not enabled") return } } recordMetadata := map[string]any{ "email": ts.Email, "project_id": ts.ProjectID, "auto": ts.Auto, "checked": ts.Checked, } fileName := geminiAuth.CredentialFileName(ts.Email, ts.ProjectID, true) record := &coreauth.Auth{ ID: fileName, Provider: "gemini", FileName: fileName, Storage: &ts, Metadata: recordMetadata, } savedPath, errSave := h.saveTokenRecord(ctx, record) if errSave != nil { log.Errorf("Failed to save token to file: %v", errSave) SetOAuthSessionError(state, "Failed to save token to file") return } CompleteOAuthSession(state) CompleteOAuthSessionsByProvider("gemini") fmt.Printf("You can now use Gemini CLI services through this CLI; token saved to %s\n", savedPath) }() c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state}) } func (h *Handler) RequestCodexToken(c *gin.Context) { ctx := context.Background() fmt.Println("Initializing Codex authentication...") // Generate PKCE codes pkceCodes, err := codex.GeneratePKCECodes() if err != nil { log.Errorf("Failed to generate PKCE codes: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate PKCE codes"}) return } // Generate random state parameter state, err := misc.GenerateRandomState() if err != nil { log.Errorf("Failed to generate state parameter: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate state parameter"}) return } // Initialize Codex auth service openaiAuth := codex.NewCodexAuth(h.cfg) // Generate authorization URL authURL, err := openaiAuth.GenerateAuthURL(state, pkceCodes) if err != nil { log.Errorf("Failed to generate authorization URL: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization url"}) return } RegisterOAuthSession(state, "codex") isWebUI := isWebUIRequest(c) var forwarder *callbackForwarder if isWebUI { targetURL, errTarget := h.managementCallbackURL("/codex/callback") if errTarget != nil { log.WithError(errTarget).Error("failed to compute codex callback target") c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"}) return } var errStart error if forwarder, errStart = startCallbackForwarder(codexCallbackPort, "codex", targetURL); errStart != nil { log.WithError(errStart).Error("failed to start codex callback forwarder") c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"}) return } } go func() { if isWebUI { defer stopCallbackForwarderInstance(codexCallbackPort, forwarder) } // Wait for callback file waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-codex-%s.oauth", state)) deadline := time.Now().Add(5 * time.Minute) var code string for { if !IsOAuthSessionPending(state, "codex") { return } if time.Now().After(deadline) { authErr := codex.NewAuthenticationError(codex.ErrCallbackTimeout, fmt.Errorf("timeout waiting for OAuth callback")) log.Error(codex.GetUserFriendlyMessage(authErr)) SetOAuthSessionError(state, "Timeout waiting for OAuth callback") return } if data, errR := os.ReadFile(waitFile); errR == nil { var m map[string]string _ = json.Unmarshal(data, &m) _ = os.Remove(waitFile) if errStr := m["error"]; errStr != "" { oauthErr := codex.NewOAuthError(errStr, "", http.StatusBadRequest) log.Error(codex.GetUserFriendlyMessage(oauthErr)) SetOAuthSessionError(state, "Bad Request") return } if m["state"] != state { authErr := codex.NewAuthenticationError(codex.ErrInvalidState, fmt.Errorf("expected %s, got %s", state, m["state"])) SetOAuthSessionError(state, "State code error") log.Error(codex.GetUserFriendlyMessage(authErr)) return } code = m["code"] break } time.Sleep(500 * time.Millisecond) } log.Debug("Authorization code received, exchanging for tokens...") // Exchange code for tokens using internal auth service bundle, errExchange := openaiAuth.ExchangeCodeForTokens(ctx, code, pkceCodes) if errExchange != nil { authErr := codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, errExchange) SetOAuthSessionError(state, "Failed to exchange authorization code for tokens") log.Errorf("Failed to exchange authorization code for tokens: %v", authErr) return } // Extract additional info for filename generation claims, _ := codex.ParseJWTToken(bundle.TokenData.IDToken) planType := "" hashAccountID := "" if claims != nil { planType = strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType) if accountID := claims.GetAccountID(); accountID != "" { digest := sha256.Sum256([]byte(accountID)) hashAccountID = hex.EncodeToString(digest[:])[:8] } } // Create token storage and persist tokenStorage := openaiAuth.CreateTokenStorage(bundle) fileName := codex.CredentialFileName(tokenStorage.Email, planType, hashAccountID, true) record := &coreauth.Auth{ ID: fileName, Provider: "codex", FileName: fileName, Storage: tokenStorage, Metadata: map[string]any{ "email": tokenStorage.Email, "account_id": tokenStorage.AccountID, }, } savedPath, errSave := h.saveTokenRecord(ctx, record) if errSave != nil { SetOAuthSessionError(state, "Failed to save authentication tokens") log.Errorf("Failed to save authentication tokens: %v", errSave) return } fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) if bundle.APIKey != "" { fmt.Println("API key obtained and saved") } fmt.Println("You can now use Codex services through this CLI") CompleteOAuthSession(state) CompleteOAuthSessionsByProvider("codex") }() c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state}) } func (h *Handler) RequestAntigravityToken(c *gin.Context) { ctx := context.Background() fmt.Println("Initializing Antigravity authentication...") authSvc := antigravity.NewAntigravityAuth(h.cfg, nil) state, errState := misc.GenerateRandomState() if errState != nil { log.Errorf("Failed to generate state parameter: %v", errState) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate state parameter"}) return } redirectURI := fmt.Sprintf("http://localhost:%d/oauth-callback", antigravity.CallbackPort) authURL := authSvc.BuildAuthURL(state, redirectURI) RegisterOAuthSession(state, "antigravity") isWebUI := isWebUIRequest(c) var forwarder *callbackForwarder if isWebUI { targetURL, errTarget := h.managementCallbackURL("/antigravity/callback") if errTarget != nil { log.WithError(errTarget).Error("failed to compute antigravity callback target") c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"}) return } var errStart error if forwarder, errStart = startCallbackForwarder(antigravity.CallbackPort, "antigravity", targetURL); errStart != nil { log.WithError(errStart).Error("failed to start antigravity callback forwarder") c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"}) return } } go func() { if isWebUI { defer stopCallbackForwarderInstance(antigravity.CallbackPort, forwarder) } waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-antigravity-%s.oauth", state)) deadline := time.Now().Add(5 * time.Minute) var authCode string for { if !IsOAuthSessionPending(state, "antigravity") { return } if time.Now().After(deadline) { log.Error("oauth flow timed out") SetOAuthSessionError(state, "OAuth flow timed out") return } if data, errReadFile := os.ReadFile(waitFile); errReadFile == nil { var payload map[string]string _ = json.Unmarshal(data, &payload) _ = os.Remove(waitFile) if errStr := strings.TrimSpace(payload["error"]); errStr != "" { log.Errorf("Authentication failed: %s", errStr) SetOAuthSessionError(state, "Authentication failed") return } if payloadState := strings.TrimSpace(payload["state"]); payloadState != "" && payloadState != state { log.Errorf("Authentication failed: state mismatch") SetOAuthSessionError(state, "Authentication failed: state mismatch") return } authCode = strings.TrimSpace(payload["code"]) if authCode == "" { log.Error("Authentication failed: code not found") SetOAuthSessionError(state, "Authentication failed: code not found") return } break } time.Sleep(500 * time.Millisecond) } tokenResp, errToken := authSvc.ExchangeCodeForTokens(ctx, authCode, redirectURI) if errToken != nil { log.Errorf("Failed to exchange token: %v", errToken) SetOAuthSessionError(state, "Failed to exchange token") return } accessToken := strings.TrimSpace(tokenResp.AccessToken) if accessToken == "" { log.Error("antigravity: token exchange returned empty access token") SetOAuthSessionError(state, "Failed to exchange token") return } email, errInfo := authSvc.FetchUserInfo(ctx, accessToken) if errInfo != nil { log.Errorf("Failed to fetch user info: %v", errInfo) SetOAuthSessionError(state, "Failed to fetch user info") return } email = strings.TrimSpace(email) if email == "" { log.Error("antigravity: user info returned empty email") SetOAuthSessionError(state, "Failed to fetch user info") return } projectID := "" if accessToken != "" { fetchedProjectID, errProject := authSvc.FetchProjectID(ctx, accessToken) if errProject != nil { log.Warnf("antigravity: failed to fetch project ID: %v", errProject) } else { projectID = fetchedProjectID log.Infof("antigravity: obtained project ID %s", projectID) } } now := time.Now() metadata := map[string]any{ "type": "antigravity", "access_token": tokenResp.AccessToken, "refresh_token": tokenResp.RefreshToken, "expires_in": tokenResp.ExpiresIn, "timestamp": now.UnixMilli(), "expired": now.Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339), } if email != "" { metadata["email"] = email } if projectID != "" { metadata["project_id"] = projectID } fileName := antigravity.CredentialFileName(email) label := strings.TrimSpace(email) if label == "" { label = "antigravity" } record := &coreauth.Auth{ ID: fileName, Provider: "antigravity", FileName: fileName, Label: label, Metadata: metadata, } savedPath, errSave := h.saveTokenRecord(ctx, record) if errSave != nil { log.Errorf("Failed to save token to file: %v", errSave) SetOAuthSessionError(state, "Failed to save token to file") return } CompleteOAuthSession(state) CompleteOAuthSessionsByProvider("antigravity") fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) if projectID != "" { fmt.Printf("Using GCP project: %s\n", projectID) } fmt.Println("You can now use Antigravity services through this CLI") }() c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state}) } func (h *Handler) RequestQwenToken(c *gin.Context) { ctx := context.Background() fmt.Println("Initializing Qwen authentication...") state := fmt.Sprintf("gem-%d", time.Now().UnixNano()) // Initialize Qwen auth service qwenAuth := qwen.NewQwenAuth(h.cfg) // Generate authorization URL deviceFlow, err := qwenAuth.InitiateDeviceFlow(ctx) if err != nil { log.Errorf("Failed to generate authorization URL: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization url"}) return } authURL := deviceFlow.VerificationURIComplete RegisterOAuthSession(state, "qwen") go func() { fmt.Println("Waiting for authentication...") tokenData, errPollForToken := qwenAuth.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier) if errPollForToken != nil { SetOAuthSessionError(state, "Authentication failed") fmt.Printf("Authentication failed: %v\n", errPollForToken) return } // Create token storage tokenStorage := qwenAuth.CreateTokenStorage(tokenData) tokenStorage.Email = fmt.Sprintf("%d", time.Now().UnixMilli()) record := &coreauth.Auth{ ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email), Provider: "qwen", FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email), Storage: tokenStorage, Metadata: map[string]any{"email": tokenStorage.Email}, } savedPath, errSave := h.saveTokenRecord(ctx, record) if errSave != nil { log.Errorf("Failed to save authentication tokens: %v", errSave) SetOAuthSessionError(state, "Failed to save authentication tokens") return } fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) fmt.Println("You can now use Qwen services through this CLI") CompleteOAuthSession(state) }() c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state}) } func (h *Handler) RequestIFlowToken(c *gin.Context) { ctx := context.Background() fmt.Println("Initializing iFlow authentication...") state := fmt.Sprintf("ifl-%d", time.Now().UnixNano()) authSvc := iflowauth.NewIFlowAuth(h.cfg) authURL, redirectURI := authSvc.AuthorizationURL(state, iflowauth.CallbackPort) RegisterOAuthSession(state, "iflow") isWebUI := isWebUIRequest(c) var forwarder *callbackForwarder if isWebUI { targetURL, errTarget := h.managementCallbackURL("/iflow/callback") if errTarget != nil { log.WithError(errTarget).Error("failed to compute iflow callback target") c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "callback server unavailable"}) return } var errStart error if forwarder, errStart = startCallbackForwarder(iflowauth.CallbackPort, "iflow", targetURL); errStart != nil { log.WithError(errStart).Error("failed to start iflow callback forwarder") c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to start callback server"}) return } } go func() { if isWebUI { defer stopCallbackForwarderInstance(iflowauth.CallbackPort, forwarder) } fmt.Println("Waiting for authentication...") waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-iflow-%s.oauth", state)) deadline := time.Now().Add(5 * time.Minute) var resultMap map[string]string for { if !IsOAuthSessionPending(state, "iflow") { return } if time.Now().After(deadline) { SetOAuthSessionError(state, "Authentication failed") fmt.Println("Authentication failed: timeout waiting for callback") return } if data, errR := os.ReadFile(waitFile); errR == nil { _ = os.Remove(waitFile) _ = json.Unmarshal(data, &resultMap) break } time.Sleep(500 * time.Millisecond) } if errStr := strings.TrimSpace(resultMap["error"]); errStr != "" { SetOAuthSessionError(state, "Authentication failed") fmt.Printf("Authentication failed: %s\n", errStr) return } if resultState := strings.TrimSpace(resultMap["state"]); resultState != state { SetOAuthSessionError(state, "Authentication failed") fmt.Println("Authentication failed: state mismatch") return } code := strings.TrimSpace(resultMap["code"]) if code == "" { SetOAuthSessionError(state, "Authentication failed") fmt.Println("Authentication failed: code missing") return } tokenData, errExchange := authSvc.ExchangeCodeForTokens(ctx, code, redirectURI) if errExchange != nil { SetOAuthSessionError(state, "Authentication failed") fmt.Printf("Authentication failed: %v\n", errExchange) return } tokenStorage := authSvc.CreateTokenStorage(tokenData) identifier := strings.TrimSpace(tokenStorage.Email) if identifier == "" { identifier = fmt.Sprintf("%d", time.Now().UnixMilli()) tokenStorage.Email = identifier } record := &coreauth.Auth{ ID: fmt.Sprintf("iflow-%s.json", identifier), Provider: "iflow", FileName: fmt.Sprintf("iflow-%s.json", identifier), Storage: tokenStorage, Metadata: map[string]any{"email": identifier, "api_key": tokenStorage.APIKey}, Attributes: map[string]string{"api_key": tokenStorage.APIKey}, } savedPath, errSave := h.saveTokenRecord(ctx, record) if errSave != nil { SetOAuthSessionError(state, "Failed to save authentication tokens") log.Errorf("Failed to save authentication tokens: %v", errSave) return } fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) if tokenStorage.APIKey != "" { fmt.Println("API key obtained and saved") } fmt.Println("You can now use iFlow services through this CLI") CompleteOAuthSession(state) CompleteOAuthSessionsByProvider("iflow") }() 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 } // 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 { 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()) } else { fileName = fmt.Sprintf("iflow-%s", fileName) } tokenStorage.Email = email timestamp := time.Now().Unix() record := &coreauth.Auth{ ID: fmt.Sprintf("%s-%d.json", fileName, timestamp), Provider: "iflow", FileName: fmt.Sprintf("%s-%d.json", fileName, timestamp), 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{}