sso_token_provider.go raw

   1  package ssocreds
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"os"
   7  	"time"
   8  
   9  	"github.com/aws/aws-sdk-go-v2/aws"
  10  	"github.com/aws/aws-sdk-go-v2/internal/sdk"
  11  	"github.com/aws/aws-sdk-go-v2/service/ssooidc"
  12  	"github.com/aws/smithy-go/auth/bearer"
  13  )
  14  
  15  // CreateTokenAPIClient provides the interface for the SSOTokenProvider's API
  16  // client for calling CreateToken operation to refresh the SSO token.
  17  type CreateTokenAPIClient interface {
  18  	CreateToken(context.Context, *ssooidc.CreateTokenInput, ...func(*ssooidc.Options)) (
  19  		*ssooidc.CreateTokenOutput, error,
  20  	)
  21  }
  22  
  23  // SSOTokenProviderOptions provides the options for configuring the
  24  // SSOTokenProvider.
  25  type SSOTokenProviderOptions struct {
  26  	// Client that can be overridden
  27  	Client CreateTokenAPIClient
  28  
  29  	// The set of API Client options to be applied when invoking the
  30  	// CreateToken operation.
  31  	ClientOptions []func(*ssooidc.Options)
  32  
  33  	// The path the file containing the cached SSO token will be read from.
  34  	// Initialized the NewSSOTokenProvider's cachedTokenFilepath parameter.
  35  	CachedTokenFilepath string
  36  }
  37  
  38  // SSOTokenProvider provides an utility for refreshing SSO AccessTokens for
  39  // Bearer Authentication. The SSOTokenProvider can only be used to refresh
  40  // already cached SSO Tokens. This utility cannot perform the initial SSO
  41  // create token.
  42  //
  43  // The SSOTokenProvider is not safe to use concurrently. It must be wrapped in
  44  // a utility such as smithy-go's auth/bearer#TokenCache. The SDK's
  45  // config.LoadDefaultConfig will automatically wrap the SSOTokenProvider with
  46  // the smithy-go TokenCache, if the external configuration loaded configured
  47  // for an SSO session.
  48  //
  49  // The initial SSO create token should be preformed with the AWS CLI before the
  50  // Go application using the SSOTokenProvider will need to retrieve the SSO
  51  // token. If the AWS CLI has not created the token cache file, this provider
  52  // will return an error when attempting to retrieve the cached token.
  53  //
  54  // This provider will attempt to refresh the cached SSO token periodically if
  55  // needed when RetrieveBearerToken is called.
  56  //
  57  // A utility such as the AWS CLI must be used to initially create the SSO
  58  // session and cached token file.
  59  // https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html
  60  type SSOTokenProvider struct {
  61  	options SSOTokenProviderOptions
  62  }
  63  
  64  var _ bearer.TokenProvider = (*SSOTokenProvider)(nil)
  65  
  66  // NewSSOTokenProvider returns an initialized SSOTokenProvider that will
  67  // periodically refresh the SSO token cached stored in the cachedTokenFilepath.
  68  // The cachedTokenFilepath file's content will be rewritten by the token
  69  // provider when the token is refreshed.
  70  //
  71  // The client must be configured for the AWS region the SSO token was created for.
  72  func NewSSOTokenProvider(client CreateTokenAPIClient, cachedTokenFilepath string, optFns ...func(o *SSOTokenProviderOptions)) *SSOTokenProvider {
  73  	options := SSOTokenProviderOptions{
  74  		Client:              client,
  75  		CachedTokenFilepath: cachedTokenFilepath,
  76  	}
  77  	for _, fn := range optFns {
  78  		fn(&options)
  79  	}
  80  
  81  	provider := &SSOTokenProvider{
  82  		options: options,
  83  	}
  84  
  85  	return provider
  86  }
  87  
  88  // RetrieveBearerToken returns the SSO token stored in the cachedTokenFilepath
  89  // the SSOTokenProvider was created with. If the token has expired
  90  // RetrieveBearerToken will attempt to refresh it. If the token cannot be
  91  // refreshed or is not present an error will be returned.
  92  //
  93  // A utility such as the AWS CLI must be used to initially create the SSO
  94  // session and cached token file. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html
  95  func (p SSOTokenProvider) RetrieveBearerToken(ctx context.Context) (bearer.Token, error) {
  96  	cachedToken, err := loadCachedToken(p.options.CachedTokenFilepath)
  97  	if err != nil {
  98  		return bearer.Token{}, err
  99  	}
 100  
 101  	if cachedToken.ExpiresAt != nil && sdk.NowTime().After(time.Time(*cachedToken.ExpiresAt)) {
 102  		cachedToken, err = p.refreshToken(ctx, cachedToken)
 103  		if err != nil {
 104  			return bearer.Token{}, fmt.Errorf("refresh cached SSO token failed, %w", err)
 105  		}
 106  	}
 107  
 108  	expiresAt := aws.ToTime((*time.Time)(cachedToken.ExpiresAt))
 109  	return bearer.Token{
 110  		Value:     cachedToken.AccessToken,
 111  		CanExpire: !expiresAt.IsZero(),
 112  		Expires:   expiresAt,
 113  	}, nil
 114  }
 115  
 116  func (p SSOTokenProvider) refreshToken(ctx context.Context, cachedToken token) (token, error) {
 117  	if cachedToken.ClientSecret == "" || cachedToken.ClientID == "" || cachedToken.RefreshToken == "" {
 118  		return token{}, fmt.Errorf("cached SSO token is expired, or not present, and cannot be refreshed")
 119  	}
 120  
 121  	createResult, err := p.options.Client.CreateToken(ctx, &ssooidc.CreateTokenInput{
 122  		ClientId:     &cachedToken.ClientID,
 123  		ClientSecret: &cachedToken.ClientSecret,
 124  		RefreshToken: &cachedToken.RefreshToken,
 125  		GrantType:    aws.String("refresh_token"),
 126  	}, p.options.ClientOptions...)
 127  	if err != nil {
 128  		return token{}, fmt.Errorf("unable to refresh SSO token, %w", err)
 129  	}
 130  
 131  	expiresAt := sdk.NowTime().Add(time.Duration(createResult.ExpiresIn) * time.Second)
 132  
 133  	cachedToken.AccessToken = aws.ToString(createResult.AccessToken)
 134  	cachedToken.ExpiresAt = (*rfc3339)(&expiresAt)
 135  	cachedToken.RefreshToken = aws.ToString(createResult.RefreshToken)
 136  
 137  	fileInfo, err := os.Stat(p.options.CachedTokenFilepath)
 138  	if err != nil {
 139  		return token{}, fmt.Errorf("failed to stat cached SSO token file %w", err)
 140  	}
 141  
 142  	if err = storeCachedToken(p.options.CachedTokenFilepath, cachedToken, fileInfo.Mode()); err != nil {
 143  		return token{}, fmt.Errorf("unable to cache refreshed SSO token, %w", err)
 144  	}
 145  
 146  	return cachedToken, nil
 147  }
 148