Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sso session and token provider support #4885

Merged
merged 43 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8855191
Squash and Merge sso token provider code and unit test
May 23, 2023
3bf12a6
Squash and Merge ssocred rfc3339 code location change
May 24, 2023
9e666ea
Squash and Merge pending changelog content
May 24, 2023
e3644bf
Deprecate smithy go dependency and Merge bearer token code
May 24, 2023
22968a0
Deprecate smithy go dependency and Merge bearer token code
May 24, 2023
6354a34
Deprecate go-cmp dependency
May 24, 2023
3e9df76
Merge changed sso token provider error format
May 24, 2023
df54980
Merge branch 'main' into feature-token-provider
May 24, 2023
14a703c
Merge modified pending changelog
May 24, 2023
6f16394
Deleted sms smoke json model that is generated from unknown merging f…
May 24, 2023
d0894cf
Modify and merge sso token provider logic
May 25, 2023
d5e1072
Merge branch 'feat-sso-session' into feature-token-provider
May 25, 2023
45be7cf
Modify and Merge changelog entry
May 25, 2023
7b47ebb
Merge branch 'feat-sso-session' into feature-token-provider
May 25, 2023
e35aaee
Modify and Merge changelog entry
May 25, 2023
3154ff0
Modify and Merge changelog entry
May 25, 2023
3c021ea
Modify and Merge changelog entry
May 26, 2023
359ee1e
Modify and Merge bearer token directory and sso token provider var name
May 26, 2023
19a523b
Modify and Merge changelog entry
May 26, 2023
da6f599
Modify and Merge changelog entry
May 26, 2023
8cf9168
Merge branch 'feat-sso-session' into feature-token-provider
wty-Bryant May 26, 2023
8affb62
Modify and Merge context used in token provider
May 26, 2023
2ffc7e2
Modify and Merge ssocreds test tag
May 30, 2023
6e4eb05
Modify and Merge ssocreds token provider test tag
May 30, 2023
b2ca51c
Modify and Merge ssocreds cached token test tag
May 30, 2023
9b2a2a1
Modify and Merge ssocreds cached token test tag
May 31, 2023
7564841
Modify and Merge ssocreds token provider test tag
May 31, 2023
1558f1a
Modify and Merge ssocreds token provider test tag
May 31, 2023
89c821a
Modify and Merge bearer token directory
May 31, 2023
1077388
Merge pull request #4853 from aws/feature-token-provider
wty-Bryant May 31, 2023
b646a31
Merge branch 'main' into feat-sso-session
May 31, 2023
29a8523
Merge branch 'main' into feat-sso-session
Jun 5, 2023
3216ed5
Update shared config logic to resolve sso section (#4868)
wty-Bryant Jun 6, 2023
c479d68
Merge branch 'main' into feat-sso-session
Jun 7, 2023
1f7d087
Merge branch 'main' into feat-sso-session
Jun 13, 2023
62eb32d
Merge branch 'main' into feat-sso-session
Jun 14, 2023
bce0677
Update sso credential provider to support token provider (#4875)
wty-Bryant Jun 15, 2023
3003a27
Merge branch 'main' into feat-sso-session
Jun 15, 2023
6e4da8f
Merge branch 'main' into feat-sso-session
Jun 20, 2023
9cabea6
Modify and Merge changelog
Jun 20, 2023
ba18cd2
Merge branch 'main' into feat-sso-session
Jul 6, 2023
d85ea03
Modify and Merge ssoCredProvider test cases and some doc
Jul 6, 2023
3ab0945
Modify and Merge sso cached token doc
Jul 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
### SDK Enhancements

### SDK Bugs
* `aws/credentials/ssocreds`: Implement SSO token provider support for `sso-session` in AWS shared config.
* Fixes [4649](https://github.com/aws/aws-sdk-go/issues/4649)
49 changes: 49 additions & 0 deletions aws/auth/bearer/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package bearer

import (
"github.com/aws/aws-sdk-go/aws"
"time"
)

type Token struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: did your IDE not complain about no comment/documentation here? this is a public type, so it should have some documentation here.

Value string

CanExpire bool
Expires time.Time
}

// Expired returns if the token's Expires time is before or equal to the time
// provided. If CanExpires is false, Expired will always return false.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// provided. If CanExpires is false, Expired will always return false.
// provided. If CanExpire is false, Expired will always return false.

func (t Token) Expired(now time.Time) bool {
if !t.CanExpire {
return false
}
now = now.Round(0)
return now.Equal(t.Expires) || now.After(t.Expires)
}

// TokenProvider provides interface for retrieving bearer tokens.
type TokenProvider interface {
RetrieveBearerToken(aws.Context) (Token, error)
}

// TokenProviderFunc provides a helper utility to wrap a function as a type
// that implements the TokenProvider interface.
type TokenProviderFunc func(aws.Context) (Token, error)

// RetrieveBearerToken calls the wrapped function, returning the Token or
// error.
func (fn TokenProviderFunc) RetrieveBearerToken(ctx aws.Context) (Token, error) {
return fn(ctx)
}

// StaticTokenProvider provides a utility for wrapping a static bearer token
// value within an implementation of a token provider.
type StaticTokenProvider struct {
Token Token
}

// RetrieveBearerToken returns the static token specified.
func (s StaticTokenProvider) RetrieveBearerToken(aws.Context) (Token, error) {
return s.Token, nil
}
75 changes: 41 additions & 34 deletions aws/credentials/ssocreds/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"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"
Expand Down Expand Up @@ -55,6 +55,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/<sha1-hex-encoded-startURL>.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
TokenProvider bearer.TokenProvider
}

// NewCredentials returns a new AWS Single Sign-On (AWS SSO) credential provider. The ConfigProvider is expected to be configured
Expand Down Expand Up @@ -89,13 +102,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.TokenProvider != nil {
token, err := p.TokenProvider.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,
})
Expand All @@ -114,32 +145,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
}

type rfc3339 time.Time

func (r *rfc3339) UnmarshalJSON(bytes []byte) error {
var value string

if err := json.Unmarshal(bytes, &value); err != nil {
return err
}

parse, err := time.Parse(time.RFC3339, value)
if err != nil {
return fmt.Errorf("expected RFC3339 timestamp: %v", err)
}

*r = rfc3339(parse)

return nil
return filepath.Join(defaultCacheLocation(), strings.ToLower(hex.EncodeToString(hash.Sum(nil)))+".json"), nil
}

type token struct {
Expand All @@ -153,13 +165,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)
}
Expand Down
114 changes: 105 additions & 9 deletions aws/credentials/ssocreds/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ package ssocreds

import (
"fmt"
"path/filepath"
"reflect"
"testing"
"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"
Expand All @@ -32,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know this is not part of your changeset. so this is just a comment for the future. but i dont think we should be including the Go testing framework object in the mocked client.

when validating the errors in the mocked client, it shouldnt directly invoke the testing object framework, but we should actually mock the returned errors, and then catch those in the test case execution

m.t.Helper()

Expand Down Expand Up @@ -88,11 +102,13 @@ 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
TokenProvider *mockTokenProvider

ExpectedErr bool
ExpectedCredentials credentials.Value
Expand Down Expand Up @@ -131,6 +147,82 @@ func TestProvider(t *testing.T) {
},
ExpectedExpire: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC),
},
"custom cached token file": {
isaiahvita marked this conversation as resolved.
Show resolved Hide resolved
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),
},
"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
},
},
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")
},
},
AccountID: "012345678901",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: whats the purpose of AccountID Region RoleName, and StartURL? removing them doesnt change the outcome of the test, and the mocked token provider doesnt seem to be reacting to them at all.

Copy link
Contributor Author

@wty-Bryant wty-Bryant Jul 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StartURL will be used to get legacy ssoToken while token provider is unavailable, AccountID and RoleName will be checked with expected value in mock SSO Client while calling GetRoleCredentialsWithContext. But some error case won't reach that step so I will just remove their fields. Additionally Region is not used in all cases so I will remove it from test case struct.

Region: "us-west-2",
RoleName: "TestRole",
StartURL: "ignored value",
ExpectedErr: true,
},
"expired access token": {
StartURL: "https://expired",
ExpectedErr: true,
Expand Down Expand Up @@ -158,10 +250,14 @@ 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,
}
if tt.TokenProvider != nil {
provider.TokenProvider = tt.TokenProvider
}

provider.Expiry.CurrentTime = nowTime
Expand Down
Loading