From dcfbec2990d3751a14517bf20b9fe554494f7c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E5=A7=9C=E6=81=92?= Date: Thu, 26 Mar 2026 11:10:07 +0800 Subject: [PATCH] feat(cursor): add management API for Cursor OAuth authentication - Add RequestCursorToken handler with PKCE + polling flow - Register /v0/management/cursor-auth-url route - Returns login URL + state for browser auth, polls in background - Saves cursor.json with access/refresh tokens on success Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/handlers/management/auth_files.go | 72 +++++++++++++++++++ internal/api/server.go | 1 + 2 files changed, 73 insertions(+) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index a0d9c159..29932669 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -29,6 +29,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot" + cursorauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor" geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" gitlabauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gitlab" iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" @@ -3694,3 +3695,74 @@ func (h *Handler) RequestKiloToken(c *gin.Context) { "verification_uri": resp.VerificationURL, }) } + +// RequestCursorToken initiates the Cursor PKCE authentication flow. +// The user opens the returned URL in a browser, logs in, and the server polls +// until the authentication completes. +func (h *Handler) RequestCursorToken(c *gin.Context) { + ctx := context.Background() + ctx = PopulateAuthContext(ctx, c) + + fmt.Println("Initializing Cursor authentication...") + + authParams, err := cursorauth.GenerateAuthParams() + if err != nil { + log.Errorf("Failed to generate Cursor auth params: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate auth params"}) + return + } + + state := fmt.Sprintf("cur-%d", time.Now().UnixNano()) + RegisterOAuthSession(state, "cursor") + + go func() { + fmt.Println("Waiting for Cursor authentication...") + fmt.Printf("Open this URL in your browser: %s\n", authParams.LoginURL) + + tokens, errPoll := cursorauth.PollForAuth(ctx, authParams.UUID, authParams.Verifier) + if errPoll != nil { + SetOAuthSessionError(state, "Authentication failed: "+errPoll.Error()) + fmt.Printf("Cursor authentication failed: %v\n", errPoll) + return + } + + // Build metadata + metadata := map[string]any{ + "type": "cursor", + "access_token": tokens.AccessToken, + "refresh_token": tokens.RefreshToken, + "timestamp": time.Now().UnixMilli(), + } + + // Extract expiry from JWT + expiry := cursorauth.GetTokenExpiry(tokens.AccessToken) + if !expiry.IsZero() { + metadata["expires_at"] = expiry.Format(time.RFC3339) + } + + fileName := "cursor.json" + record := &coreauth.Auth{ + ID: fileName, + Provider: "cursor", + FileName: fileName, + Label: "Cursor User", + Metadata: metadata, + } + savedPath, errSave := h.saveTokenRecord(ctx, record) + if errSave != nil { + log.Errorf("Failed to save Cursor tokens: %v", errSave) + SetOAuthSessionError(state, "Failed to save tokens") + return + } + + fmt.Printf("Cursor authentication successful! Token saved to %s\n", savedPath) + CompleteOAuthSession(state) + CompleteOAuthSessionsByProvider("cursor") + }() + + c.JSON(200, gin.H{ + "status": "ok", + "url": authParams.LoginURL, + "state": state, + }) +} diff --git a/internal/api/server.go b/internal/api/server.go index 2a63c97c..95327eef 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -682,6 +682,7 @@ func (s *Server) registerManagementRoutes() { mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken) mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken) mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken) + mgmt.GET("/cursor-auth-url", s.mgmt.RequestCursorToken) mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken) mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback) mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)