From 0a6e3a6c29dca105c44f508d094f51da7eaee94d Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Wed, 7 Jun 2023 14:04:29 -0400 Subject: [PATCH 1/5] Update and Merge logic of sso credential provider to support token provider --- aws/credentials/ssocreds/provider.go | 54 +++++++++++++++++------ aws/credentials/ssocreds/provider_test.go | 50 +++++++++++++++++---- aws/session/credentials.go | 21 ++++++++- 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/aws/credentials/ssocreds/provider.go b/aws/credentials/ssocreds/provider.go index 95de5520f09..0366faa2c9b 100644 --- a/aws/credentials/ssocreds/provider.go +++ b/aws/credentials/ssocreds/provider.go @@ -54,6 +54,19 @@ type Provider struct { // The URL that points to the organization's AWS Single Sign-On (AWS SSO) user portal. StartURL string + + // The filepath the cached token will be retrieved from. If unset Provider will + // use the startURL to determine the filepath at. + // + // ~/.aws/sso/cache/.json + // + // If custom cached token filepath is used, the Provider's startUrl + // parameter will be ignored. + CachedTokenFilepath string + + // Used by the SSOCredentialProvider if a token configuration + // profile is used in the shared config + SSOTokenProvider *SSOTokenProvider } // NewCredentials returns a new AWS Single Sign-On (AWS SSO) credential provider. The ConfigProvider is expected to be configured @@ -88,13 +101,31 @@ func (p *Provider) Retrieve() (credentials.Value, error) { // RetrieveWithContext retrieves temporary AWS credentials from the configured Amazon Single Sign-On (AWS SSO) user portal // by exchanging the accessToken present in ~/.aws/sso/cache. func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) { - tokenFile, err := loadTokenFile(p.StartURL) - if err != nil { - return credentials.Value{}, err + var accessToken *string + if p.SSOTokenProvider != nil { + token, err := p.SSOTokenProvider.RetrieveBearerToken(ctx) + if err != nil { + return credentials.Value{}, err + } + accessToken = &token.Value + } else { + if p.CachedTokenFilepath == "" { + cachedTokenFilePath, err := getCachedFilePath(p.StartURL) + if err != nil { + return credentials.Value{}, err + } + p.CachedTokenFilepath = cachedTokenFilePath + } + + tokenFile, err := loadTokenFile(p.CachedTokenFilepath) + if err != nil { + return credentials.Value{}, err + } + accessToken = &tokenFile.AccessToken } output, err := p.Client.GetRoleCredentialsWithContext(ctx, &sso.GetRoleCredentialsInput{ - AccessToken: &tokenFile.AccessToken, + AccessToken: accessToken, AccountId: &p.AccountID, RoleName: &p.RoleName, }) @@ -113,13 +144,13 @@ func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Val }, nil } -func getCacheFileName(url string) (string, error) { +func getCachedFilePath(startUrl string) (string, error) { hash := sha1.New() - _, err := hash.Write([]byte(url)) + _, err := hash.Write([]byte(startUrl)) if err != nil { return "", err } - return strings.ToLower(hex.EncodeToString(hash.Sum(nil))) + ".json", nil + return filepath.Join(defaultCacheLocation(), strings.ToLower(hex.EncodeToString(hash.Sum(nil)))+".json"), nil } type token struct { @@ -133,13 +164,8 @@ func (t token) Expired() bool { return nowTime().Round(0).After(time.Time(t.ExpiresAt)) } -func loadTokenFile(startURL string) (t token, err error) { - key, err := getCacheFileName(startURL) - if err != nil { - return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, err) - } - - fileBytes, err := ioutil.ReadFile(filepath.Join(defaultCacheLocation(), key)) +func loadTokenFile(cachedTokenPath string) (t token, err error) { + fileBytes, err := ioutil.ReadFile(cachedTokenPath) if err != nil { return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, err) } diff --git a/aws/credentials/ssocreds/provider_test.go b/aws/credentials/ssocreds/provider_test.go index 0548d60325a..57f0890601c 100644 --- a/aws/credentials/ssocreds/provider_test.go +++ b/aws/credentials/ssocreds/provider_test.go @@ -5,6 +5,7 @@ package ssocreds import ( "fmt" + "path/filepath" "reflect" "testing" "time" @@ -88,11 +89,12 @@ func TestProvider(t *testing.T) { defer restoreTime() cases := map[string]struct { - Client mockClient - AccountID string - Region string - RoleName string - StartURL string + Client mockClient + AccountID string + Region string + RoleName string + StartURL string + CachedTokenFilePath string ExpectedErr bool ExpectedCredentials credentials.Value @@ -131,6 +133,35 @@ func TestProvider(t *testing.T) { }, ExpectedExpire: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC), }, + "custom cached token file": { + Client: mockClient{ + ExpectedAccountID: "012345678901", + ExpectedRoleName: "TestRole", + ExpectedAccessToken: "ZhbHVldGhpcyBpcyBub3QgYSByZWFsIH", + Response: func(mock mockClient) (*sso.GetRoleCredentialsOutput, error) { + return &sso.GetRoleCredentialsOutput{ + RoleCredentials: &sso.RoleCredentials{ + AccessKeyId: aws.String("AccessKey"), + SecretAccessKey: aws.String("SecretKey"), + SessionToken: aws.String("SessionToken"), + Expiration: aws.Int64(1611177743123), + }, + }, nil + }, + }, + CachedTokenFilePath: filepath.Join("testdata", "custom_cached_token.json"), + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "ignored value", + ExpectedCredentials: credentials.Value{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretKey", + SessionToken: "SessionToken", + ProviderName: ProviderName, + }, + ExpectedExpire: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC), + }, "expired access token": { StartURL: "https://expired", ExpectedErr: true, @@ -158,10 +189,11 @@ func TestProvider(t *testing.T) { tt.Client.t = t provider := &Provider{ - Client: tt.Client, - AccountID: tt.AccountID, - RoleName: tt.RoleName, - StartURL: tt.StartURL, + Client: tt.Client, + AccountID: tt.AccountID, + RoleName: tt.RoleName, + StartURL: tt.StartURL, + CachedTokenFilepath: tt.CachedTokenFilePath, } provider.Expiry.CurrentTime = nowTime diff --git a/aws/session/credentials.go b/aws/session/credentials.go index 1d3f4c3adc3..50c726d62eb 100644 --- a/aws/session/credentials.go +++ b/aws/session/credentials.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go/aws/defaults" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/internal/shareddefaults" + "github.com/aws/aws-sdk-go/service/ssooidc" "github.com/aws/aws-sdk-go/service/sts" ) @@ -173,8 +174,25 @@ func resolveSSOCredentials(cfg *aws.Config, sharedCfg sharedConfig, handlers req return nil, err } + var optFns []func(provider *ssocreds.Provider) cfgCopy := cfg.Copy() - cfgCopy.Region = &sharedCfg.SSORegion + + if sharedCfg.SSOSession != nil { + cfgCopy.Region = &sharedCfg.SSOSession.SSORegion + cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedCfg.SSOSession.Name) + if err != nil { + return nil, err + } + mySession := Must(NewSession()) + oidcClient := ssooidc.New(mySession, cfgCopy) + tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath) + optFns = append(optFns, func(p *ssocreds.Provider) { + p.SSOTokenProvider = tokenProvider + p.CachedTokenFilepath = cachedPath + }) + } else { + cfgCopy.Region = &sharedCfg.SSORegion + } return ssocreds.NewCredentials( &Session{ @@ -184,6 +202,7 @@ func resolveSSOCredentials(cfg *aws.Config, sharedCfg sharedConfig, handlers req sharedCfg.SSOAccountID, sharedCfg.SSORoleName, sharedCfg.SSOStartURL, + optFns..., ), nil } From ae875c16350e6f3642e3d6c159bae75bca055899 Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Wed, 7 Jun 2023 14:27:17 -0400 Subject: [PATCH 2/5] Add and Merge sso credential provider unit test data --- aws/credentials/ssocreds/testdata/custom_cached_token.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 aws/credentials/ssocreds/testdata/custom_cached_token.json diff --git a/aws/credentials/ssocreds/testdata/custom_cached_token.json b/aws/credentials/ssocreds/testdata/custom_cached_token.json new file mode 100644 index 00000000000..4b83e28fdc9 --- /dev/null +++ b/aws/credentials/ssocreds/testdata/custom_cached_token.json @@ -0,0 +1,4 @@ +{ + "accessToken": "ZhbHVldGhpcyBpcyBub3QgYSByZWFsIH", + "expiresAt": "2021-01-19T23:00:00Z" +} From 175106b48267e6e9b2ebe0987c86fd0ea32545fc Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Tue, 13 Jun 2023 12:55:34 -0400 Subject: [PATCH 3/5] Modify and Merge sso credential provider's token provider field and unit test --- aws/credentials/ssocreds/provider.go | 10 ++-- aws/credentials/ssocreds/provider_test.go | 66 +++++++++++++++++++++++ aws/session/credentials.go | 5 +- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/aws/credentials/ssocreds/provider.go b/aws/credentials/ssocreds/provider.go index 0366faa2c9b..35586980552 100644 --- a/aws/credentials/ssocreds/provider.go +++ b/aws/credentials/ssocreds/provider.go @@ -10,6 +10,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/auth/bearer" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/credentials" @@ -64,9 +65,12 @@ type Provider struct { // parameter will be ignored. CachedTokenFilepath string + // Used by the SSOCredentialProvider to judge if TokenProvider is configured + HasTokenProvider bool + // Used by the SSOCredentialProvider if a token configuration // profile is used in the shared config - SSOTokenProvider *SSOTokenProvider + TokenProvider bearer.TokenProvider } // NewCredentials returns a new AWS Single Sign-On (AWS SSO) credential provider. The ConfigProvider is expected to be configured @@ -102,8 +106,8 @@ func (p *Provider) Retrieve() (credentials.Value, error) { // by exchanging the accessToken present in ~/.aws/sso/cache. func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) { var accessToken *string - if p.SSOTokenProvider != nil { - token, err := p.SSOTokenProvider.RetrieveBearerToken(ctx) + if p.HasTokenProvider { + token, err := p.TokenProvider.RetrieveBearerToken(ctx) if err != nil { return credentials.Value{}, err } diff --git a/aws/credentials/ssocreds/provider_test.go b/aws/credentials/ssocreds/provider_test.go index 57f0890601c..2efba49e209 100644 --- a/aws/credentials/ssocreds/provider_test.go +++ b/aws/credentials/ssocreds/provider_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/auth/bearer" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/sso" @@ -33,6 +34,18 @@ type mockClient struct { Response func(mockClient) (*sso.GetRoleCredentialsOutput, error) } +type mockTokenProvider struct { + Response func() (bearer.Token, error) +} + +func (p mockTokenProvider) RetrieveBearerToken(ctx aws.Context) (bearer.Token, error) { + if p.Response == nil { + return bearer.Token{}, nil + } + + return p.Response() +} + func (m mockClient) GetRoleCredentialsWithContext(ctx aws.Context, params *sso.GetRoleCredentialsInput, _ ...request.Option) (*sso.GetRoleCredentialsOutput, error) { m.t.Helper() @@ -95,6 +108,8 @@ func TestProvider(t *testing.T) { RoleName string StartURL string CachedTokenFilePath string + HasTokenProvider bool + TokenProvider mockTokenProvider ExpectedErr bool ExpectedCredentials credentials.Value @@ -162,6 +177,55 @@ func TestProvider(t *testing.T) { }, ExpectedExpire: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC), }, + "access token retrieved by token provider": { + Client: mockClient{ + ExpectedAccountID: "012345678901", + ExpectedRoleName: "TestRole", + ExpectedAccessToken: "WFsIHZhbHVldGhpcyBpcyBub3QgYSByZ", + Response: func(mock mockClient) (*sso.GetRoleCredentialsOutput, error) { + return &sso.GetRoleCredentialsOutput{ + RoleCredentials: &sso.RoleCredentials{ + AccessKeyId: aws.String("AccessKey"), + SecretAccessKey: aws.String("SecretKey"), + SessionToken: aws.String("SessionToken"), + Expiration: aws.Int64(1611177743123), + }, + }, nil + }, + }, + TokenProvider: mockTokenProvider{ + Response: func() (bearer.Token, error) { + return bearer.Token{ + Value: "WFsIHZhbHVldGhpcyBpcyBub3QgYSByZ", + }, nil + }, + }, + HasTokenProvider: true, + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "ignored value", + ExpectedCredentials: credentials.Value{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretKey", + SessionToken: "SessionToken", + ProviderName: ProviderName, + }, + ExpectedExpire: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC), + }, + "token provider return error": { + TokenProvider: mockTokenProvider{ + Response: func() (bearer.Token, error) { + return bearer.Token{}, fmt.Errorf("mock token provider return error") + }, + }, + HasTokenProvider: true, + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "ignored value", + ExpectedErr: true, + }, "expired access token": { StartURL: "https://expired", ExpectedErr: true, @@ -194,6 +258,8 @@ func TestProvider(t *testing.T) { RoleName: tt.RoleName, StartURL: tt.StartURL, CachedTokenFilepath: tt.CachedTokenFilePath, + HasTokenProvider: tt.HasTokenProvider, + TokenProvider: tt.TokenProvider, } provider.Expiry.CurrentTime = nowTime diff --git a/aws/session/credentials.go b/aws/session/credentials.go index 50c726d62eb..b124d753f34 100644 --- a/aws/session/credentials.go +++ b/aws/session/credentials.go @@ -34,7 +34,7 @@ func resolveCredentials(cfg *aws.Config, switch { case len(sessOpts.Profile) != 0: - // User explicitly provided an Profile in the session's configuration + // User explicitly provided a Profile in the session's configuration // so load that profile from shared config first. // Github(aws/aws-sdk-go#2727) return resolveCredsFromProfile(cfg, envCfg, sharedCfg, handlers, sessOpts) @@ -187,7 +187,8 @@ func resolveSSOCredentials(cfg *aws.Config, sharedCfg sharedConfig, handlers req oidcClient := ssooidc.New(mySession, cfgCopy) tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath) optFns = append(optFns, func(p *ssocreds.Provider) { - p.SSOTokenProvider = tokenProvider + p.HasTokenProvider = true + p.TokenProvider = *tokenProvider p.CachedTokenFilepath = cachedPath }) } else { From 9d68ffdce30be147cc2b2ab862889fb7c1a3ecae Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Wed, 14 Jun 2023 11:34:24 -0400 Subject: [PATCH 4/5] Modify and Merge sso credential provider's token provider and unit test --- aws/credentials/ssocreds/provider.go | 5 +---- aws/credentials/ssocreds/provider_test.go | 13 +++++++------ aws/credentials/ssocreds/token_provider.go | 6 +++--- aws/session/credentials.go | 3 +-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/aws/credentials/ssocreds/provider.go b/aws/credentials/ssocreds/provider.go index 35586980552..4138e725dde 100644 --- a/aws/credentials/ssocreds/provider.go +++ b/aws/credentials/ssocreds/provider.go @@ -65,9 +65,6 @@ type Provider struct { // parameter will be ignored. CachedTokenFilepath string - // Used by the SSOCredentialProvider to judge if TokenProvider is configured - HasTokenProvider bool - // Used by the SSOCredentialProvider if a token configuration // profile is used in the shared config TokenProvider bearer.TokenProvider @@ -106,7 +103,7 @@ func (p *Provider) Retrieve() (credentials.Value, error) { // by exchanging the accessToken present in ~/.aws/sso/cache. func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) { var accessToken *string - if p.HasTokenProvider { + if p.TokenProvider != nil { token, err := p.TokenProvider.RetrieveBearerToken(ctx) if err != nil { return credentials.Value{}, err diff --git a/aws/credentials/ssocreds/provider_test.go b/aws/credentials/ssocreds/provider_test.go index 2efba49e209..0b046a7e140 100644 --- a/aws/credentials/ssocreds/provider_test.go +++ b/aws/credentials/ssocreds/provider_test.go @@ -38,7 +38,7 @@ type mockTokenProvider struct { Response func() (bearer.Token, error) } -func (p mockTokenProvider) RetrieveBearerToken(ctx aws.Context) (bearer.Token, error) { +func (p *mockTokenProvider) RetrieveBearerToken(ctx aws.Context) (bearer.Token, error) { if p.Response == nil { return bearer.Token{}, nil } @@ -109,7 +109,7 @@ func TestProvider(t *testing.T) { StartURL string CachedTokenFilePath string HasTokenProvider bool - TokenProvider mockTokenProvider + TokenProvider *mockTokenProvider ExpectedErr bool ExpectedCredentials credentials.Value @@ -193,7 +193,7 @@ func TestProvider(t *testing.T) { }, nil }, }, - TokenProvider: mockTokenProvider{ + TokenProvider: &mockTokenProvider{ Response: func() (bearer.Token, error) { return bearer.Token{ Value: "WFsIHZhbHVldGhpcyBpcyBub3QgYSByZ", @@ -214,7 +214,7 @@ func TestProvider(t *testing.T) { ExpectedExpire: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC), }, "token provider return error": { - TokenProvider: mockTokenProvider{ + TokenProvider: &mockTokenProvider{ Response: func() (bearer.Token, error) { return bearer.Token{}, fmt.Errorf("mock token provider return error") }, @@ -258,8 +258,9 @@ func TestProvider(t *testing.T) { RoleName: tt.RoleName, StartURL: tt.StartURL, CachedTokenFilepath: tt.CachedTokenFilePath, - HasTokenProvider: tt.HasTokenProvider, - TokenProvider: tt.TokenProvider, + } + if tt.TokenProvider != nil { + provider.TokenProvider = tt.TokenProvider } provider.Expiry.CurrentTime = nowTime diff --git a/aws/credentials/ssocreds/token_provider.go b/aws/credentials/ssocreds/token_provider.go index 2ca4babc936..7562cd01350 100644 --- a/aws/credentials/ssocreds/token_provider.go +++ b/aws/credentials/ssocreds/token_provider.go @@ -2,11 +2,11 @@ package ssocreds import ( "fmt" - "github.com/aws/aws-sdk-go/aws/auth/bearer" "os" "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/auth/bearer" "github.com/aws/aws-sdk-go/service/ssooidc" ) @@ -76,7 +76,7 @@ func NewSSOTokenProvider(client CreateTokenAPIClient, cachedTokenFilepath string // // 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 aws.Context) (bearer.Token, error) { +func (p *SSOTokenProvider) RetrieveBearerToken(ctx aws.Context) (bearer.Token, error) { cachedToken, err := loadCachedToken(p.options.CachedTokenFilepath) if err != nil { return bearer.Token{}, err @@ -97,7 +97,7 @@ func (p SSOTokenProvider) RetrieveBearerToken(ctx aws.Context) (bearer.Token, er }, nil } -func (p SSOTokenProvider) refreshToken(token cachedToken) (cachedToken, error) { +func (p *SSOTokenProvider) refreshToken(token cachedToken) (cachedToken, error) { if token.ClientSecret == "" || token.ClientID == "" || token.RefreshToken == "" { return cachedToken{}, fmt.Errorf("cached SSO token is expired, or not present, and cannot be refreshed") } diff --git a/aws/session/credentials.go b/aws/session/credentials.go index b124d753f34..304061158a5 100644 --- a/aws/session/credentials.go +++ b/aws/session/credentials.go @@ -187,8 +187,7 @@ func resolveSSOCredentials(cfg *aws.Config, sharedCfg sharedConfig, handlers req oidcClient := ssooidc.New(mySession, cfgCopy) tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath) optFns = append(optFns, func(p *ssocreds.Provider) { - p.HasTokenProvider = true - p.TokenProvider = *tokenProvider + p.TokenProvider = tokenProvider p.CachedTokenFilepath = cachedPath }) } else { From 2fb8973f68eac07f5f7fd0ac05fb36c9aa0270c3 Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Thu, 15 Jun 2023 14:02:20 -0400 Subject: [PATCH 5/5] Modify and Merge sso credential provider's unit test --- aws/credentials/ssocreds/provider_test.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/aws/credentials/ssocreds/provider_test.go b/aws/credentials/ssocreds/provider_test.go index 0b046a7e140..64ef6b704e2 100644 --- a/aws/credentials/ssocreds/provider_test.go +++ b/aws/credentials/ssocreds/provider_test.go @@ -108,7 +108,6 @@ func TestProvider(t *testing.T) { RoleName string StartURL string CachedTokenFilePath string - HasTokenProvider bool TokenProvider *mockTokenProvider ExpectedErr bool @@ -200,11 +199,10 @@ func TestProvider(t *testing.T) { }, nil }, }, - HasTokenProvider: true, - AccountID: "012345678901", - Region: "us-west-2", - RoleName: "TestRole", - StartURL: "ignored value", + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "ignored value", ExpectedCredentials: credentials.Value{ AccessKeyID: "AccessKey", SecretAccessKey: "SecretKey", @@ -219,12 +217,11 @@ func TestProvider(t *testing.T) { return bearer.Token{}, fmt.Errorf("mock token provider return error") }, }, - HasTokenProvider: true, - AccountID: "012345678901", - Region: "us-west-2", - RoleName: "TestRole", - StartURL: "ignored value", - ExpectedErr: true, + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "ignored value", + ExpectedErr: true, }, "expired access token": { StartURL: "https://expired",