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  }