github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/auth/middleware.go (about) 1 package auth 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strings" 9 "sync" 10 11 "github.com/anthropics/anthropic-sdk-go/internal/requestconfig" 12 ) 13 14 // applyBearerAuth sets the Authorization and anthropic-beta headers for 15 // OAuth/bearer-based credentials on outgoing API requests. 16 func applyBearerAuth(req *http.Request, token string) { 17 req.Header.Set("Authorization", "Bearer "+token) 18 existing := req.Header.Get("anthropic-beta") 19 switch { 20 case existing == "": 21 req.Header.Set("anthropic-beta", OAuthAPIBetaHeader) 22 case !strings.Contains(existing, OAuthAPIBetaHeader): 23 req.Header.Set("anthropic-beta", existing+","+OAuthAPIBetaHeader) 24 } 25 } 26 27 type authMiddlewareFunc func(*http.Request, func(*http.Request) (*http.Response, error)) (*http.Response, error) 28 29 // NewMiddleware returns bearer-auth middleware serving tokens from cache. 30 // Tokens are exchanged at, and only attached to requests for, cfg.BaseURL — 31 // a request path may be an absolute URL for some other host. 32 func NewMiddleware(cache *TokenCache, cfg *requestconfig.RequestConfig) authMiddlewareFunc { 33 return func(req *http.Request, next func(*http.Request) (*http.Response, error)) (*http.Response, error) { 34 // Skip if static auth headers are already set. 35 if req.Header.Get("X-Api-Key") != "" || req.Header.Get("Authorization") != "" { 36 return next(req) 37 } 38 39 if cfg.BaseURL == nil { 40 return nil, errors.New("cannot resolve credentials: client base URL is not set") 41 } 42 if !sameOrigin(req.URL, cfg.BaseURL) { 43 return next(req) 44 } 45 baseURL := cfg.BaseURL.Scheme + "://" + cfg.BaseURL.Host 46 47 token, err := cache.Token(req.Context(), baseURL) 48 if err != nil { 49 return nil, fmt.Errorf("failed to get credentials token: %w", err) 50 } 51 applyBearerAuth(req, token) 52 53 resp, err := next(req) 54 if err != nil { 55 return resp, err 56 } 57 58 if resp.StatusCode != http.StatusUnauthorized { 59 return resp, nil 60 } 61 62 if !cache.Invalidate() { 63 return resp, nil 64 } 65 66 if req.GetBody == nil && req.Body != nil { 67 warnOnce( 68 "401-retry-unreplayable-body", 69 "401 response; request body cannot be replayed (no GetBody), skipping credential refresh retry. Use a buffered body or fix credentials.", 70 ) 71 return resp, nil 72 } 73 74 token, tokenErr := cache.ForceToken(req.Context(), baseURL) 75 if tokenErr != nil { 76 warnOnce( 77 "401-retry-refresh-failed", 78 "401 response; credential refresh also failed, returning original 401: %v", 79 tokenErr, 80 ) 81 return resp, nil 82 } 83 applyBearerAuth(req, token) 84 85 if req.GetBody != nil { 86 newBody, bodyErr := req.GetBody() 87 if bodyErr != nil { 88 resp.Body.Close() 89 return nil, bodyErr 90 } 91 req.Body = newBody 92 } 93 94 resp.Body.Close() 95 return next(req) 96 } 97 } 98 99 func sameOrigin(u, base *url.URL) bool { 100 return strings.EqualFold(u.Scheme, base.Scheme) && strings.EqualFold(u.Host, base.Host) 101 } 102 103 // clientKey identifies the HTTP transport a [TokenCache] should bind to. 104 // Keying the per-option cache map on this struct ensures two clients that 105 // share the same [WithAuthMiddleware] option value each get their own cache 106 // bound to their own transport, rather than the second client silently 107 // reusing the first client's transport for token exchange. 108 type clientKey struct { 109 httpClient *http.Client 110 customDoer requestconfig.HTTPDoer 111 } 112 113 // WithAuthMiddleware returns a [requestconfig.RequestOptionFunc] that appends 114 // HTTP middleware which authenticates requests using a cached bearer token. 115 // 116 // A separate [TokenCache] is lazily constructed per distinct HTTP transport 117 // (the combination of [requestconfig.RequestConfig.HTTPClient] and 118 // [requestconfig.RequestConfig.CustomHTTPDoer]) that applies this option, so 119 // the same option value can safely be shared across multiple clients built 120 // with different transports. Within one client, the cache is constructed 121 // exactly once and reused for every request so its in-memory token, refresh 122 // state, and 401 invalidation signal are preserved. 123 // 124 // The base URL is read at request time, so this option can be listed in any 125 // order relative to [option.WithBaseURL] / [option.WithEnvironmentProduction]. 126 // 127 // If the request already has an X-Api-Key or Authorization header set 128 // (e.g. via [option.WithAPIKey] or [option.WithAuthToken]), the middleware 129 // is a no-op pass-through. 130 func WithAuthMiddleware(provider TokenProvider) requestconfig.RequestOptionFunc { 131 var ( 132 mu sync.Mutex 133 byKey = map[clientKey]*TokenCache{} 134 ) 135 return func(r *requestconfig.RequestConfig) error { 136 key := clientKey{httpClient: r.HTTPClient, customDoer: r.CustomHTTPDoer} 137 138 mu.Lock() 139 cache, ok := byKey[key] 140 if !ok { 141 handler := r.HTTPClient.Do 142 if r.CustomHTTPDoer != nil { 143 handler = r.CustomHTTPDoer.Do 144 } 145 cache = NewTokenCache(provider, handler) 146 byKey[key] = cache 147 } 148 mu.Unlock() 149 150 r.Middlewares = append(r.Middlewares, NewMiddleware(cache, r)) 151 return nil 152 } 153 }