package claude import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/luispater/CLIProxyAPI/internal/config" "github.com/luispater/CLIProxyAPI/internal/util" log "github.com/sirupsen/logrus" ) const ( anthropicAuthURL = "https://claude.ai/oauth/authorize" anthropicTokenURL = "https://console.anthropic.com/v1/oauth/token" anthropicClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" redirectURI = "http://localhost:54545/callback" ) // Parse token response type tokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` Organization struct { UUID string `json:"uuid"` Name string `json:"name"` } `json:"organization"` Account struct { UUID string `json:"uuid"` EmailAddress string `json:"email_address"` } `json:"account"` } // ClaudeAuth handles Anthropic OAuth2 authentication flow type ClaudeAuth struct { httpClient *http.Client } // NewClaudeAuth creates a new Anthropic authentication service func NewClaudeAuth(cfg *config.Config) *ClaudeAuth { return &ClaudeAuth{ httpClient: util.SetProxy(cfg, &http.Client{}), } } // GenerateAuthURL creates the OAuth authorization URL with PKCE func (o *ClaudeAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string, string, error) { if pkceCodes == nil { return "", "", fmt.Errorf("PKCE codes are required") } params := url.Values{ "code": {"true"}, "client_id": {anthropicClientID}, "response_type": {"code"}, "redirect_uri": {redirectURI}, "scope": {"org:create_api_key user:profile user:inference"}, "code_challenge": {pkceCodes.CodeChallenge}, "code_challenge_method": {"S256"}, "state": {state}, } authURL := fmt.Sprintf("%s?%s", anthropicAuthURL, params.Encode()) return authURL, state, nil } func (c *ClaudeAuth) parseCodeAndState(code string) (parsedCode, parsedState string) { splits := strings.Split(code, "#") parsedCode = splits[0] if len(splits) > 1 { parsedState = splits[1] } return } // ExchangeCodeForTokens exchanges authorization code for access tokens func (o *ClaudeAuth) ExchangeCodeForTokens(ctx context.Context, code, state string, pkceCodes *PKCECodes) (*ClaudeAuthBundle, error) { if pkceCodes == nil { return nil, fmt.Errorf("PKCE codes are required for token exchange") } newCode, newState := o.parseCodeAndState(code) // Prepare token exchange request reqBody := map[string]interface{}{ "code": newCode, "state": state, "grant_type": "authorization_code", "client_id": anthropicClientID, "redirect_uri": redirectURI, "code_verifier": pkceCodes.CodeVerifier, } // Include state if present if newState != "" { reqBody["state"] = newState } jsonBody, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } // log.Debugf("Token exchange request: %s", string(jsonBody)) req, err := http.NewRequestWithContext(ctx, "POST", anthropicTokenURL, strings.NewReader(string(jsonBody))) if err != nil { return nil, fmt.Errorf("failed to create token request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := o.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("token exchange request failed: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read token response: %w", err) } // log.Debugf("Token response: %s", string(body)) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, string(body)) } // log.Debugf("Token response: %s", string(body)) var tokenResp tokenResponse if err = json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("failed to parse token response: %w", err) } // Create token data tokenData := ClaudeTokenData{ AccessToken: tokenResp.AccessToken, RefreshToken: tokenResp.RefreshToken, Email: tokenResp.Account.EmailAddress, Expire: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339), } // Create auth bundle bundle := &ClaudeAuthBundle{ TokenData: tokenData, LastRefresh: time.Now().Format(time.RFC3339), } return bundle, nil } // RefreshTokens refreshes the access token using the refresh token func (o *ClaudeAuth) RefreshTokens(ctx context.Context, refreshToken string) (*ClaudeTokenData, error) { if refreshToken == "" { return nil, fmt.Errorf("refresh token is required") } reqBody := map[string]interface{}{ "client_id": anthropicClientID, "grant_type": "refresh_token", "refresh_token": refreshToken, } jsonBody, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", anthropicTokenURL, strings.NewReader(string(jsonBody))) if err != nil { return nil, fmt.Errorf("failed to create refresh request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := o.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("token refresh request failed: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read refresh response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("token refresh failed with status %d: %s", resp.StatusCode, string(body)) } // log.Debugf("Token response: %s", string(body)) var tokenResp tokenResponse if err = json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("failed to parse token response: %w", err) } // Create token data return &ClaudeTokenData{ AccessToken: tokenResp.AccessToken, RefreshToken: tokenResp.RefreshToken, Email: tokenResp.Account.EmailAddress, Expire: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339), }, nil } // CreateTokenStorage creates a new ClaudeTokenStorage from auth bundle and user info func (o *ClaudeAuth) CreateTokenStorage(bundle *ClaudeAuthBundle) *ClaudeTokenStorage { storage := &ClaudeTokenStorage{ AccessToken: bundle.TokenData.AccessToken, RefreshToken: bundle.TokenData.RefreshToken, LastRefresh: bundle.LastRefresh, Email: bundle.TokenData.Email, Expire: bundle.TokenData.Expire, } return storage } // RefreshTokensWithRetry refreshes tokens with automatic retry logic func (o *ClaudeAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken string, maxRetries int) (*ClaudeTokenData, error) { var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { // Wait before retry select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(time.Duration(attempt) * time.Second): } } tokenData, err := o.RefreshTokens(ctx, refreshToken) if err == nil { return tokenData, nil } lastErr = err log.Warnf("Token refresh attempt %d failed: %v", attempt+1, err) } return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr) } // UpdateTokenStorage updates an existing token storage with new token data func (o *ClaudeAuth) UpdateTokenStorage(storage *ClaudeTokenStorage, tokenData *ClaudeTokenData) { storage.AccessToken = tokenData.AccessToken storage.RefreshToken = tokenData.RefreshToken storage.LastRefresh = time.Now().Format(time.RFC3339) storage.Email = tokenData.Email storage.Expire = tokenData.Expire }