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 all 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)
50 changes: 50 additions & 0 deletions aws/auth/bearer/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package bearer

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

// Token provides a type wrapping a bearer token and expiration metadata.
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 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
129 changes: 106 additions & 23 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 @@ -24,14 +26,25 @@ type mockClient struct {
Output *sso.GetRoleCredentialsOutput
Err error

ExpectedAccountID string
ExpectedAccessToken string
ExpectedRoleName string
ExpectedClientRegion string
ExpectedAccountID string
ExpectedAccessToken string
ExpectedRoleName string

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 +101,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
RoleName string
StartURL string
CachedTokenFilePath string
TokenProvider *mockTokenProvider

ExpectedErr bool
ExpectedCredentials credentials.Value
Expand All @@ -104,10 +118,9 @@ func TestProvider(t *testing.T) {
},
"valid required parameter values": {
Client: mockClient{
ExpectedAccountID: "012345678901",
ExpectedRoleName: "TestRole",
ExpectedClientRegion: "us-west-2",
ExpectedAccessToken: "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl",
ExpectedAccountID: "012345678901",
ExpectedRoleName: "TestRole",
ExpectedAccessToken: "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl",
Response: func(mock mockClient) (*sso.GetRoleCredentialsOutput, error) {
return &sso.GetRoleCredentialsOutput{
RoleCredentials: &sso.RoleCredentials{
Expand All @@ -120,7 +133,6 @@ func TestProvider(t *testing.T) {
},
},
AccountID: "012345678901",
Region: "us-west-2",
RoleName: "TestRole",
StartURL: "https://valid-required-only",
ExpectedCredentials: credentials.Value{
Expand All @@ -131,22 +143,89 @@ 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",
RoleName: "TestRole",
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",
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")
},
},
ExpectedErr: true,
},
"expired access token": {
StartURL: "https://expired",
ExpectedErr: true,
},
"api error": {
Client: mockClient{
ExpectedAccountID: "012345678901",
ExpectedRoleName: "TestRole",
ExpectedClientRegion: "us-west-2",
ExpectedAccessToken: "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl",
ExpectedAccountID: "012345678901",
ExpectedRoleName: "TestRole",
ExpectedAccessToken: "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl",
Response: func(mock mockClient) (*sso.GetRoleCredentialsOutput, error) {
return nil, fmt.Errorf("api error")
},
},
AccountID: "012345678901",
Region: "us-west-2",
RoleName: "TestRole",
StartURL: "https://valid-required-only",
ExpectedErr: true,
Expand All @@ -158,10 +237,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