sso_token_provider.go 5.4 KB

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