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 }