package auth import ( "bytes" "context" "encoding/json" "fmt" "io" "net" "net/http" "os" "path/filepath" "sync" "time" tls "github.com/refraction-networking/utls" "github.com/rs/zerolog/log" "golang.org/x/net/http2" ) const ( tokenEndpoint = "https://platform.claude.com/v1/oauth/token" clientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" oauthScopes = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload" refreshLead = 5 * time.Minute refreshInterval = 30 * time.Second refreshBackoff = 5 * time.Minute ) var utlsClient = newUTLSClient() type tokenRequest struct { ClientID string `json:"client_id"` GrantType string `json:"grant_type"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` } 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"` } func RefreshToken(ctx context.Context, cred *Credential) error { if cred.RefreshToken == "" { return fmt.Errorf("no refresh token") } reqBody, _ := json.Marshal(tokenRequest{ ClientID: clientID, GrantType: "refresh_token", RefreshToken: cred.RefreshToken, Scope: oauthScopes, }) req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, bytes.NewReader(reqBody)) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") log.Debug(). Str("url", tokenEndpoint). Str("grant_type", "refresh_token"). Str("client_id", clientID). Str("scope", oauthScopes). Msg("token refresh request") resp, err := utlsClient.Do(req) if err != nil { return fmt.Errorf("execute request: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) log.Debug(). Int("status", resp.StatusCode). Int("response_size", len(body)). Msg("token refresh response") if resp.StatusCode != http.StatusOK { return fmt.Errorf("refresh returned %d: %s", resp.StatusCode, string(body)) } var tokenResp tokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return fmt.Errorf("decode response: %w", err) } cred.mu.Lock() cred.AccessToken = tokenResp.AccessToken if tokenResp.RefreshToken != "" { 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() filePath := cred.FilePath accessToken := cred.AccessToken refreshToken := cred.RefreshToken expiresAt := cred.ExpiresAt cred.mu.Unlock() if filePath == "" { return nil } var doc map[string]any raw, err := os.ReadFile(filePath) if err != nil { if !os.IsNotExist(err) { return err } // File doesn't exist yet (cold start) — create from scratch if mkdirErr := os.MkdirAll(filepath.Dir(filePath), 0700); mkdirErr != nil { return fmt.Errorf("create credential dir: %w", mkdirErr) } doc = make(map[string]any) } else { if err := json.Unmarshal(raw, &doc); err != nil { return err } } oauth, _ := doc["claudeAiOauth"].(map[string]any) if oauth == nil { oauth = make(map[string]any) } oauth["accessToken"] = accessToken oauth["refreshToken"] = refreshToken oauth["expiresAt"] = expiresAt.UnixMilli() doc["claudeAiOauth"] = oauth out, _ := json.MarshalIndent(doc, "", " ") return os.WriteFile(filePath, out, 0600) } func newUTLSClient() *http.Client { return &http.Client{ Timeout: 15 * time.Second, Transport: &utlsRefreshTransport{}, } } type utlsRefreshTransport struct { mu sync.Mutex conn *http2.ClientConn host string } func (t *utlsRefreshTransport) RoundTrip(req *http.Request) (*http.Response, error) { host := req.URL.Hostname() port := req.URL.Port() if port == "" { port = "443" } t.mu.Lock() if t.conn != nil && t.host == host && t.conn.CanTakeNewRequest() { conn := t.conn t.mu.Unlock() resp, err := conn.RoundTrip(req) if err == nil { return resp, nil } t.mu.Lock() t.conn = nil t.mu.Unlock() } else { t.mu.Unlock() } addr := net.JoinHostPort(host, port) rawConn, err := net.DialTimeout("tcp", addr, 10*time.Second) if err != nil { return nil, err } tlsConn := tls.UClient(rawConn, &tls.Config{ServerName: host}, tls.HelloChrome_Auto) if err := tlsConn.Handshake(); err != nil { rawConn.Close() return nil, err } h2Conn, err := (&http2.Transport{}).NewClientConn(tlsConn) if err != nil { tlsConn.Close() return nil, err } t.mu.Lock() t.conn = h2Conn t.host = host t.mu.Unlock() return h2Conn.RoundTrip(req) } func StartBackgroundRefresh(ctx context.Context, pool *Pool) { go func() { for { select { case <-ctx.Done(): log.Info().Msg("background refresh stopped") return case <-time.After(refreshInterval): refreshExpiring(pool) } } }() } func refreshExpiring(pool *Pool) { pool.mu.Lock() creds := make([]*Credential, len(pool.creds)) copy(creds, pool.creds) pool.mu.Unlock() threshold := time.Now().Add(refreshLead) for _, cred := range creds { cred.mu.Lock() needsRefresh := !cred.ExpiresAt.IsZero() && cred.ExpiresAt.Before(threshold) hasRefresh := cred.RefreshToken != "" nextRetry := cred.nextRefreshAfter email := cred.Email expiresAt := cred.ExpiresAt cred.mu.Unlock() if !hasRefresh || !needsRefresh { continue } if !nextRetry.IsZero() && time.Now().Before(nextRetry) { continue } log.Info().Str("credential", email).Time("expires_at", expiresAt).Msg("refreshing token") ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) err := RefreshToken(ctx, cred) cancel() if err != nil { log.Error().Err(err).Str("credential", email).Msg("token refresh failed") cred.mu.Lock() cred.nextRefreshAfter = time.Now().Add(refreshBackoff) cred.mu.Unlock() } else { cred.mu.Lock() newExpiresAt := cred.ExpiresAt cred.nextRefreshAfter = time.Time{} cred.mu.Unlock() log.Info().Str("credential", email).Time("new_expiry", newExpiresAt).Msg("token refreshed") } } }