Files
anthropic-proxy/internal/auth/refresh.go
T
Alexander c4c1d4daa4 Anthropic API proxy with OAuth credential rotation and Claude Code fingerprinting
Sniffs a real Claude Code request on startup to capture exact HTTP headers,
then replays them for all proxied requests. Injects the billing header with
per-request SHA256 fingerprint into the system prompt. Uses utls with Chrome
TLS fingerprint to pass Cloudflare's bot detection on api.anthropic.com.

Supports both streaming (SSE) and non-streaming modes, round-robin credential
selection with automatic failover, and loading OAuth tokens from both
cli-proxy-api auth files and native ~/.claude/.credentials.json.
2026-04-09 21:05:32 +02:00

110 lines
2.7 KiB
Go

package auth
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const (
tokenEndpoint = "https://api.anthropic.com/v1/oauth/token"
clientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
)
type tokenRequest struct {
ClientID string `json:"client_id"`
GrantType string `json:"grant_type"`
RefreshToken string `json:"refresh_token"`
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
Account struct {
EmailAddress string `json:"email_address"`
} `json:"account"`
}
type authFileJSON struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Email string `json:"email"`
Expired string `json:"expired"`
Type string `json:"type"`
}
// RefreshToken performs an OAuth token refresh for the given credential.
func RefreshToken(ctx context.Context, cred *Credential) error {
reqBody := tokenRequest{
ClientID: clientID,
GrantType: "refresh_token",
RefreshToken: cred.RefreshToken,
}
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal refresh request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("create refresh request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("execute refresh request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("refresh failed with status %d", resp.StatusCode)
}
var tokenResp tokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return fmt.Errorf("decode refresh response: %w", err)
}
cred.mu.Lock()
cred.AccessToken = tokenResp.AccessToken
cred.RefreshToken = tokenResp.RefreshToken
cred.ExpiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
if tokenResp.Account.EmailAddress != "" {
cred.Email = tokenResp.Account.EmailAddress
}
cred.mu.Unlock()
return persistCredential(cred)
}
func persistCredential(cred *Credential) error {
cred.mu.Lock()
data := authFileJSON{
AccessToken: cred.AccessToken,
RefreshToken: cred.RefreshToken,
Email: cred.Email,
Expired: cred.ExpiresAt.Format(time.RFC3339),
Type: "claude",
}
filePath := cred.FilePath
cred.mu.Unlock()
out, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("marshal auth file: %w", err)
}
if err := os.WriteFile(filePath, out, 0600); err != nil {
return fmt.Errorf("write auth file %s: %w", filePath, err)
}
return nil
}