1package ssocreds
2
3import (
4 "context"
5 "time"
6
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/sso"
10)
11
12// ProviderName is the name of the provider used to specify the source of
13// credentials.
14const ProviderName = "SSOProvider"
15
16// GetRoleCredentialsAPIClient is a API client that implements the
17// GetRoleCredentials operation.
18type GetRoleCredentialsAPIClient interface {
19 GetRoleCredentials(context.Context, *sso.GetRoleCredentialsInput, ...func(*sso.Options)) (
20 *sso.GetRoleCredentialsOutput, error,
21 )
22}
23
24// Options is the Provider options structure.
25type Options struct {
26 // The Client which is configured for the AWS Region where the AWS SSO user
27 // portal is located.
28 Client GetRoleCredentialsAPIClient
29
30 // The AWS account that is assigned to the user.
31 AccountID string
32
33 // The role name that is assigned to the user.
34 RoleName string
35
36 // The URL that points to the organization's AWS Single Sign-On (AWS SSO)
37 // user portal.
38 StartURL string
39
40 // The filepath the cached token will be retrieved from. If unset Provider will
41 // use the startURL to determine the filepath at.
42 //
43 // ~/.aws/sso/cache/<sha1-hex-encoded-startURL>.json
44 //
45 // If custom cached token filepath is used, the Provider's startUrl
46 // parameter will be ignored.
47 CachedTokenFilepath string
48
49 // Used by the SSOCredentialProvider if a token configuration
50 // profile is used in the shared config
51 SSOTokenProvider *SSOTokenProvider
52}
53
54// Provider is an AWS credential provider that retrieves temporary AWS
55// credentials by exchanging an SSO login token.
56type Provider struct {
57 options Options
58
59 cachedTokenFilepath string
60}
61
62// New returns a new AWS Single Sign-On (AWS SSO) credential provider. The
63// provided client is expected to be configured for the AWS Region where the
64// AWS SSO user portal is located.
65func New(client GetRoleCredentialsAPIClient, accountID, roleName, startURL string, optFns ...func(options *Options)) *Provider {
66 options := Options{
67 Client: client,
68 AccountID: accountID,
69 RoleName: roleName,
70 StartURL: startURL,
71 }
72
73 for _, fn := range optFns {
74 fn(&options)
75 }
76
77 return &Provider{
78 options: options,
79 cachedTokenFilepath: options.CachedTokenFilepath,
80 }
81}
82
83// Retrieve retrieves temporary AWS credentials from the configured Amazon
84// Single Sign-On (AWS SSO) user portal by exchanging the accessToken present
85// in ~/.aws/sso/cache. However, if a token provider configuration exists
86// in the shared config, then we ought to use the token provider rather then
87// direct access on the cached token.
88func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
89 var accessToken *string
90 if p.options.SSOTokenProvider != nil {
91 token, err := p.options.SSOTokenProvider.RetrieveBearerToken(ctx)
92 if err != nil {
93 return aws.Credentials{}, err
94 }
95 accessToken = &token.Value
96 } else {
97 if p.cachedTokenFilepath == "" {
98 cachedTokenFilepath, err := StandardCachedTokenFilepath(p.options.StartURL)
99 if err != nil {
100 return aws.Credentials{}, &InvalidTokenError{Err: err}
101 }
102 p.cachedTokenFilepath = cachedTokenFilepath
103 }
104
105 tokenFile, err := loadCachedToken(p.cachedTokenFilepath)
106 if err != nil {
107 return aws.Credentials{}, &InvalidTokenError{Err: err}
108 }
109
110 if tokenFile.ExpiresAt == nil || sdk.NowTime().After(time.Time(*tokenFile.ExpiresAt)) {
111 return aws.Credentials{}, &InvalidTokenError{}
112 }
113 accessToken = &tokenFile.AccessToken
114 }
115
116 output, err := p.options.Client.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{
117 AccessToken: accessToken,
118 AccountId: &p.options.AccountID,
119 RoleName: &p.options.RoleName,
120 })
121 if err != nil {
122 return aws.Credentials{}, err
123 }
124
125 return aws.Credentials{
126 AccessKeyID: aws.ToString(output.RoleCredentials.AccessKeyId),
127 SecretAccessKey: aws.ToString(output.RoleCredentials.SecretAccessKey),
128 SessionToken: aws.ToString(output.RoleCredentials.SessionToken),
129 CanExpire: true,
130 Expires: time.Unix(0, output.RoleCredentials.Expiration*int64(time.Millisecond)).UTC(),
131 Source: ProviderName,
132 AccountID: p.options.AccountID,
133 }, nil
134}
135
136// InvalidTokenError is the error type that is returned if loaded token has
137// expired or is otherwise invalid. To refresh the SSO session run AWS SSO
138// login with the corresponding profile.
139type InvalidTokenError struct {
140 Err error
141}
142
143func (i *InvalidTokenError) Unwrap() error {
144 return i.Err
145}
146
147func (i *InvalidTokenError) Error() string {
148 const msg = "the SSO session has expired or is invalid"
149 if i.Err == nil {
150 return msg
151 }
152 return msg + ": " + i.Err.Error()
153}