1package ssocreds
2
3import (
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.
17type 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.
25type 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
60type SSOTokenProvider struct {
61 options SSOTokenProviderOptions
62}
63
64var _ 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.
72func 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
95func (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
116func (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}