| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- package bearer
- import (
- "context"
- "fmt"
- "sync/atomic"
- "time"
- smithycontext "github.com/aws/smithy-go/context"
- "github.com/aws/smithy-go/internal/sync/singleflight"
- )
- // package variable that can be override in unit tests.
- var timeNow = time.Now
- // TokenCacheOptions provides a set of optional configuration options for the
- // TokenCache TokenProvider.
- type TokenCacheOptions struct {
- // The duration before the token will expire when the credentials will be
- // refreshed. If DisableAsyncRefresh is true, the RetrieveBearerToken calls
- // will be blocking.
- //
- // Asynchronous refreshes are deduplicated, and only one will be in-flight
- // at a time. If the token expires while an asynchronous refresh is in
- // flight, the next call to RetrieveBearerToken will block on that refresh
- // to return.
- RefreshBeforeExpires time.Duration
- // The timeout the underlying TokenProvider's RetrieveBearerToken call must
- // return within, or will be canceled. Defaults to 0, no timeout.
- //
- // If 0 timeout, its possible for the underlying tokenProvider's
- // RetrieveBearerToken call to block forever. Preventing subsequent
- // TokenCache attempts to refresh the token.
- //
- // If this timeout is reached all pending deduplicated calls to
- // TokenCache RetrieveBearerToken will fail with an error.
- RetrieveBearerTokenTimeout time.Duration
- // The minimum duration between asynchronous refresh attempts. If the next
- // asynchronous recent refresh attempt was within the minimum delay
- // duration, the call to retrieve will return the current cached token, if
- // not expired.
- //
- // The asynchronous retrieve is deduplicated across multiple calls when
- // RetrieveBearerToken is called. The asynchronous retrieve is not a
- // periodic task. It is only performed when the token has not yet expired,
- // and the current item is within the RefreshBeforeExpires window, and the
- // TokenCache's RetrieveBearerToken method is called.
- //
- // If 0, (default) there will be no minimum delay between asynchronous
- // refresh attempts.
- //
- // If DisableAsyncRefresh is true, this option is ignored.
- AsyncRefreshMinimumDelay time.Duration
- // Sets if the TokenCache will attempt to refresh the token in the
- // background asynchronously instead of blocking for credentials to be
- // refreshed. If disabled token refresh will be blocking.
- //
- // The first call to RetrieveBearerToken will always be blocking, because
- // there is no cached token.
- DisableAsyncRefresh bool
- }
- // TokenCache provides an utility to cache Bearer Authentication tokens from a
- // wrapped TokenProvider. The TokenCache can be has options to configure the
- // cache's early and asynchronous refresh of the token.
- type TokenCache struct {
- options TokenCacheOptions
- provider TokenProvider
- cachedToken atomic.Value
- lastRefreshAttemptTime atomic.Value
- sfGroup singleflight.Group
- }
- // NewTokenCache returns a initialized TokenCache that implements the
- // TokenProvider interface. Wrapping the provider passed in. Also taking a set
- // of optional functional option parameters to configure the token cache.
- func NewTokenCache(provider TokenProvider, optFns ...func(*TokenCacheOptions)) *TokenCache {
- var options TokenCacheOptions
- for _, fn := range optFns {
- fn(&options)
- }
- return &TokenCache{
- options: options,
- provider: provider,
- }
- }
- // RetrieveBearerToken returns the token if it could be obtained, or error if a
- // valid token could not be retrieved.
- //
- // The passed in Context's cancel/deadline/timeout will impacting only this
- // individual retrieve call and not any other already queued up calls. This
- // means underlying provider's RetrieveBearerToken calls could block for ever,
- // and not be canceled with the Context. Set RetrieveBearerTokenTimeout to
- // provide a timeout, preventing the underlying TokenProvider blocking forever.
- //
- // By default, if the passed in Context is canceled, all of its values will be
- // considered expired. The wrapped TokenProvider will not be able to lookup the
- // values from the Context once it is expired. This is done to protect against
- // expired values no longer being valid. To disable this behavior, use
- // smithy-go's context.WithPreserveExpiredValues to add a value to the Context
- // before calling RetrieveBearerToken to enable support for expired values.
- //
- // Without RetrieveBearerTokenTimeout there is the potential for a underlying
- // Provider's RetrieveBearerToken call to sit forever. Blocking in subsequent
- // attempts at refreshing the token.
- func (p *TokenCache) RetrieveBearerToken(ctx context.Context) (Token, error) {
- cachedToken, ok := p.getCachedToken()
- if !ok || cachedToken.Expired(timeNow()) {
- return p.refreshBearerToken(ctx)
- }
- // Check if the token should be refreshed before it expires.
- refreshToken := cachedToken.Expired(timeNow().Add(p.options.RefreshBeforeExpires))
- if !refreshToken {
- return cachedToken, nil
- }
- if p.options.DisableAsyncRefresh {
- return p.refreshBearerToken(ctx)
- }
- p.tryAsyncRefresh(ctx)
- return cachedToken, nil
- }
- // tryAsyncRefresh attempts to asynchronously refresh the token returning the
- // already cached token. If it AsyncRefreshMinimumDelay option is not zero, and
- // the duration since the last refresh is less than that value, nothing will be
- // done.
- func (p *TokenCache) tryAsyncRefresh(ctx context.Context) {
- if p.options.AsyncRefreshMinimumDelay != 0 {
- var lastRefreshAttempt time.Time
- if v := p.lastRefreshAttemptTime.Load(); v != nil {
- lastRefreshAttempt = v.(time.Time)
- }
- if timeNow().Before(lastRefreshAttempt.Add(p.options.AsyncRefreshMinimumDelay)) {
- return
- }
- }
- // Ignore the returned channel so this won't be blocking, and limit the
- // number of additional goroutines created.
- p.sfGroup.DoChan("async-refresh", func() (interface{}, error) {
- res, err := p.refreshBearerToken(ctx)
- if p.options.AsyncRefreshMinimumDelay != 0 {
- var refreshAttempt time.Time
- if err != nil {
- refreshAttempt = timeNow()
- }
- p.lastRefreshAttemptTime.Store(refreshAttempt)
- }
- return res, err
- })
- }
- func (p *TokenCache) refreshBearerToken(ctx context.Context) (Token, error) {
- resCh := p.sfGroup.DoChan("refresh-token", func() (interface{}, error) {
- ctx := smithycontext.WithSuppressCancel(ctx)
- if v := p.options.RetrieveBearerTokenTimeout; v != 0 {
- var cancel func()
- ctx, cancel = context.WithTimeout(ctx, v)
- defer cancel()
- }
- return p.singleRetrieve(ctx)
- })
- select {
- case res := <-resCh:
- return res.Val.(Token), res.Err
- case <-ctx.Done():
- return Token{}, fmt.Errorf("retrieve bearer token canceled, %w", ctx.Err())
- }
- }
- func (p *TokenCache) singleRetrieve(ctx context.Context) (interface{}, error) {
- token, err := p.provider.RetrieveBearerToken(ctx)
- if err != nil {
- return Token{}, fmt.Errorf("failed to retrieve bearer token, %w", err)
- }
- p.cachedToken.Store(&token)
- return token, nil
- }
- // getCachedToken returns the currently cached token and true if found. Returns
- // false if no token is cached.
- func (p *TokenCache) getCachedToken() (Token, bool) {
- v := p.cachedToken.Load()
- if v == nil {
- return Token{}, false
- }
- t := v.(*Token)
- if t == nil || t.Value == "" {
- return Token{}, false
- }
- return *t, true
- }
|