claude_token_provider.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "strings"
  7. "time"
  8. )
  9. const (
  10. claudeTokenRefreshSkew = 3 * time.Minute
  11. claudeTokenCacheSkew = 5 * time.Minute
  12. claudeLockWaitTime = 200 * time.Millisecond
  13. )
  14. // ClaudeTokenCache token cache interface.
  15. type ClaudeTokenCache = GeminiTokenCache
  16. // ClaudeTokenProvider manages access_token for Claude OAuth and Vertex service account accounts.
  17. type ClaudeTokenProvider struct {
  18. accountRepo AccountRepository
  19. tokenCache ClaudeTokenCache
  20. oauthService *OAuthService
  21. refreshAPI *OAuthRefreshAPI
  22. executor OAuthRefreshExecutor
  23. refreshPolicy ProviderRefreshPolicy
  24. }
  25. func NewClaudeTokenProvider(
  26. accountRepo AccountRepository,
  27. tokenCache ClaudeTokenCache,
  28. oauthService *OAuthService,
  29. ) *ClaudeTokenProvider {
  30. return &ClaudeTokenProvider{
  31. accountRepo: accountRepo,
  32. tokenCache: tokenCache,
  33. oauthService: oauthService,
  34. refreshPolicy: ClaudeProviderRefreshPolicy(),
  35. }
  36. }
  37. // SetRefreshAPI injects unified OAuth refresh API and executor.
  38. func (p *ClaudeTokenProvider) SetRefreshAPI(api *OAuthRefreshAPI, executor OAuthRefreshExecutor) {
  39. p.refreshAPI = api
  40. p.executor = executor
  41. }
  42. // SetRefreshPolicy injects caller-side refresh policy.
  43. func (p *ClaudeTokenProvider) SetRefreshPolicy(policy ProviderRefreshPolicy) {
  44. p.refreshPolicy = policy
  45. }
  46. // GetAccessToken returns a valid access_token.
  47. func (p *ClaudeTokenProvider) GetAccessToken(ctx context.Context, account *Account) (string, error) {
  48. if account == nil {
  49. return "", errors.New("account is nil")
  50. }
  51. if account.Platform != PlatformAnthropic || (account.Type != AccountTypeOAuth && account.Type != AccountTypeServiceAccount) {
  52. return "", errors.New("not an anthropic oauth or service account")
  53. }
  54. if account.Type == AccountTypeServiceAccount {
  55. return p.getServiceAccountAccessToken(ctx, account)
  56. }
  57. if intelligentContext(ctx) != nil {
  58. return intelligentExistingAccessToken(account)
  59. }
  60. cacheKey := ClaudeTokenCacheKey(account)
  61. // 1) Try cache first.
  62. if p.tokenCache != nil {
  63. if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
  64. slog.Debug("claude_token_cache_hit", "account_id", account.ID)
  65. return token, nil
  66. } else if err != nil {
  67. slog.Warn("claude_token_cache_get_failed", "account_id", account.ID, "error", err)
  68. }
  69. }
  70. slog.Debug("claude_token_cache_miss", "account_id", account.ID)
  71. // 2) Refresh if needed (pre-expiry skew).
  72. expiresAt := account.GetCredentialAsTime("expires_at")
  73. needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= claudeTokenRefreshSkew
  74. refreshFailed := false
  75. if needsRefresh && p.refreshAPI != nil && p.executor != nil {
  76. result, err := p.refreshAPI.RefreshIfNeeded(ctx, account, p.executor, claudeTokenRefreshSkew)
  77. if err != nil {
  78. if p.refreshPolicy.OnRefreshError == ProviderRefreshErrorReturn {
  79. return "", err
  80. }
  81. slog.Warn("claude_token_refresh_failed", "account_id", account.ID, "error", err)
  82. refreshFailed = true
  83. } else if result.LockHeld {
  84. if p.refreshPolicy.OnLockHeld == ProviderLockHeldWaitForCache && p.tokenCache != nil {
  85. time.Sleep(claudeLockWaitTime)
  86. if token, cacheErr := p.tokenCache.GetAccessToken(ctx, cacheKey); cacheErr == nil && strings.TrimSpace(token) != "" {
  87. slog.Debug("claude_token_cache_hit_after_wait", "account_id", account.ID)
  88. return token, nil
  89. }
  90. }
  91. } else {
  92. account = result.Account
  93. expiresAt = account.GetCredentialAsTime("expires_at")
  94. }
  95. } else if needsRefresh && p.tokenCache != nil {
  96. // Backward-compatible test path when refreshAPI is not injected.
  97. locked, lockErr := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
  98. if lockErr == nil && locked {
  99. defer func() { _ = p.tokenCache.ReleaseRefreshLock(ctx, cacheKey) }()
  100. } else if lockErr != nil {
  101. slog.Warn("claude_token_lock_failed", "account_id", account.ID, "error", lockErr)
  102. } else {
  103. time.Sleep(claudeLockWaitTime)
  104. if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
  105. slog.Debug("claude_token_cache_hit_after_wait", "account_id", account.ID)
  106. return token, nil
  107. }
  108. }
  109. }
  110. accessToken := account.GetCredential("access_token")
  111. if strings.TrimSpace(accessToken) == "" {
  112. return "", errors.New("access_token not found in credentials")
  113. }
  114. // 3) Populate cache with TTL.
  115. if p.tokenCache != nil {
  116. latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
  117. if isStale && latestAccount != nil {
  118. slog.Debug("claude_token_version_stale_use_latest", "account_id", account.ID)
  119. accessToken = latestAccount.GetCredential("access_token")
  120. if strings.TrimSpace(accessToken) == "" {
  121. return "", errors.New("access_token not found after version check")
  122. }
  123. } else {
  124. ttl := 30 * time.Minute
  125. if refreshFailed {
  126. if p.refreshPolicy.FailureTTL > 0 {
  127. ttl = p.refreshPolicy.FailureTTL
  128. } else {
  129. ttl = time.Minute
  130. }
  131. slog.Debug("claude_token_cache_short_ttl", "account_id", account.ID, "reason", "refresh_failed")
  132. } else if expiresAt != nil {
  133. until := time.Until(*expiresAt)
  134. switch {
  135. case until > claudeTokenCacheSkew:
  136. ttl = until - claudeTokenCacheSkew
  137. case until > 0:
  138. ttl = until
  139. default:
  140. ttl = time.Minute
  141. }
  142. }
  143. if err := p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl); err != nil {
  144. slog.Warn("claude_token_cache_set_failed", "account_id", account.ID, "error", err)
  145. }
  146. }
  147. }
  148. return accessToken, nil
  149. }
  150. func (p *ClaudeTokenProvider) getServiceAccountAccessToken(ctx context.Context, account *Account) (string, error) {
  151. return getVertexServiceAccountAccessToken(ctx, p.tokenCache, account)
  152. }