mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-04 19:51:18 +00:00
Previously Cursor required a manual ?label=xxx parameter to distinguish accounts (unlike Codex which auto-generates filenames from JWT claims). Cursor JWTs contain a "sub" claim (e.g. "auth0|user_XXXX") that uniquely identifies each account. Now we: - Add ParseJWTSub() + SubToShortHash() to extract and hash the sub claim - Refactor GetTokenExpiry() to share the new decodeJWTPayload() helper - Update CredentialFileName(label, subHash) to auto-generate filenames from the sub hash when no explicit label is provided (e.g. "cursor.8f202e67.json" instead of always "cursor.json") - Add DisplayLabel() for human-readable account identification - Store "sub" in metadata for observability - Update both management API handler and SDK authenticator Same account always produces the same filename (deterministic), different accounts get different files. Explicit ?label= still takes priority. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
cursorauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// CursorAuthenticator implements OAuth PKCE login for Cursor.
|
|
type CursorAuthenticator struct{}
|
|
|
|
// NewCursorAuthenticator constructs a new Cursor authenticator.
|
|
func NewCursorAuthenticator() Authenticator {
|
|
return &CursorAuthenticator{}
|
|
}
|
|
|
|
// Provider returns the provider key for cursor.
|
|
func (CursorAuthenticator) Provider() string {
|
|
return "cursor"
|
|
}
|
|
|
|
// RefreshLead returns the time before expiry when a refresh should be attempted.
|
|
func (CursorAuthenticator) RefreshLead() *time.Duration {
|
|
d := 10 * time.Minute
|
|
return &d
|
|
}
|
|
|
|
// Login initiates the Cursor PKCE authentication flow.
|
|
func (a CursorAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("cursor auth: configuration is required")
|
|
}
|
|
if opts == nil {
|
|
opts = &LoginOptions{}
|
|
}
|
|
|
|
// Generate PKCE auth parameters
|
|
authParams, err := cursorauth.GenerateAuthParams()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cursor: failed to generate auth params: %w", err)
|
|
}
|
|
|
|
// Display the login URL
|
|
log.Info("Starting Cursor authentication...")
|
|
log.Infof("Please visit this URL to log in: %s", authParams.LoginURL)
|
|
|
|
// Try to open the browser automatically
|
|
if !opts.NoBrowser {
|
|
if browser.IsAvailable() {
|
|
if errOpen := browser.OpenURL(authParams.LoginURL); errOpen != nil {
|
|
log.Warnf("Failed to open browser automatically: %v", errOpen)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Info("Waiting for Cursor authorization...")
|
|
|
|
// Poll for the auth result
|
|
tokens, err := cursorauth.PollForAuth(ctx, authParams.UUID, authParams.Verifier)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cursor: authentication failed: %w", err)
|
|
}
|
|
|
|
expiresAt := cursorauth.GetTokenExpiry(tokens.AccessToken)
|
|
|
|
// Auto-identify account from JWT sub claim
|
|
sub := cursorauth.ParseJWTSub(tokens.AccessToken)
|
|
subHash := cursorauth.SubToShortHash(sub)
|
|
|
|
log.Info("Cursor authentication successful!")
|
|
|
|
metadata := map[string]any{
|
|
"type": "cursor",
|
|
"access_token": tokens.AccessToken,
|
|
"refresh_token": tokens.RefreshToken,
|
|
"expires_at": expiresAt.Format(time.RFC3339),
|
|
"timestamp": time.Now().UnixMilli(),
|
|
}
|
|
if sub != "" {
|
|
metadata["sub"] = sub
|
|
}
|
|
|
|
fileName := cursorauth.CredentialFileName("", subHash)
|
|
|
|
return &coreauth.Auth{
|
|
ID: fileName,
|
|
Provider: a.Provider(),
|
|
FileName: fileName,
|
|
Label: cursorauth.DisplayLabel("", subHash),
|
|
Metadata: metadata,
|
|
}, nil
|
|
}
|