| 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...") |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| anthropicAuth := claude.NewClaudeAuth(h.cfg) |
|
|
| |
| 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) |
| } |
|
|
| |
| 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...") |
| |
| 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 |
| } |
|
|
| |
| rawCode := resultMap["code"] |
| code := strings.Split(rawCode, "#")[0] |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
|
|
| |
| projectID := c.Query("project_id") |
|
|
| fmt.Println("Initializing Google authentication...") |
|
|
| |
| conf := &oauth2.Config{ |
| ClientID: geminiAuth.ClientID, |
| ClientSecret: geminiAuth.ClientSecret, |
| RedirectURL: fmt.Sprintf("http://localhost:%d/oauth2callback", geminiAuth.DefaultCallbackPort), |
| Scopes: geminiAuth.Scopes, |
| Endpoint: google.Endpoint, |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
|
|
| |
| 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") |
| } |
|
|
| |
| 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 == "", |
| } |
|
|
| |
| 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...") |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| openaiAuth := codex.NewCodexAuth(h.cfg) |
|
|
| |
| 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) |
| } |
|
|
| |
| 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...") |
| |
| 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 |
| } |
|
|
| |
| 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] |
| } |
| } |
|
|
| |
| 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()) |
| |
| qwenAuth := qwen.NewQwenAuth(h.cfg) |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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{} |
|
|