code.dwrz.net

Go monorepo.
Log | Files | Refs

sso_token_provider.go (5522B)


      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 }