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