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 token refresh support to SSOCredentialProvider #1903

Merged
merged 22 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8d09ba9
first pass of code written, untested
isaiahvita Oct 26, 2022
fce3a5f
fix tokenprovider check on sharedconfig #1846
isaiahvita Oct 26, 2022
5eb9d79
sep says only sso session section required
isaiahvita Oct 26, 2022
edb4d43
get naem from session instead of profile per the TP SEP
isaiahvita Oct 26, 2022
db6bfac
add potential todos
isaiahvita Oct 26, 2022
6118eb4
check if sso-session section is present, fixes #1845
isaiahvita Oct 26, 2022
ca45169
remove comment
isaiahvita Oct 26, 2022
fe74417
fix shared config sso validations
isaiahvita Oct 26, 2022
02d654c
200 from sso, shared config and resolver logic wrong tho
isaiahvita Oct 27, 2022
6d34685
200 from sso, but unit tests failing
isaiahvita Oct 27, 2022
cf28002
fixed issues with existing failing sso test cases, still need to add …
isaiahvita Oct 29, 2022
c210422
add token provider options to sso creds provider
isaiahvita Oct 31, 2022
7607b30
added happy path test case
isaiahvita Nov 1, 2022
a6d628f
added error unit test, failing on prior unit test
isaiahvita Nov 2, 2022
ef79153
added missing region unit test, moved sso validation to cred resoluti…
isaiahvita Nov 2, 2022
f374d28
add region mismatch unit test
isaiahvita Nov 2, 2022
54f2645
remove dev comments
isaiahvita Nov 2, 2022
a4c2dd8
fix validation for token provider config
isaiahvita Nov 2, 2022
f4fb486
remove dev comment
isaiahvita Nov 2, 2022
2ae06ad
fix style issues, replaced tokenclient with tokenprovider as option
isaiahvita Nov 8, 2022
3a82c79
change name of token provider option
isaiahvita Nov 10, 2022
2f0c3b1
add changelog files for feature and announcement
isaiahvita Nov 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changelog/2f60f375046b466789bff94b7406ba45.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "2f60f375-046b-4667-89bf-f94b7406ba45",
"type": "feature",
"description": "Adds token refresh support (via SSOTokenProvider) when using the SSOCredentialProvider",
"modules": [
"config",
"credentials"
]
}
9 changes: 9 additions & 0 deletions .changelog/e5c705b5a4f34a99a85f12cee232ae51.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "e5c705b5-a4f3-4a99-a85f-12cee232ae51",
"type": "announcement",
"description": "When using the SSOTokenProvider, a previous implementation incorrectly compensated for invalid SSOTokenProvider configurations in the shared profile. This has been fixed via PR #1903 and tracked in issue #1846",
"modules": [
"config",
"credentials"
]
}
15 changes: 2 additions & 13 deletions config/resolve_bearer_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,9 @@ func resolveBearerAuthTokenProviderChain(ctx context.Context, cfg *aws.Config, c

var provider smithybearer.TokenProvider

if sharedConfig.SSOSession != nil || (sharedConfig.SSORegion != "" && sharedConfig.SSOStartURL != "") {
ssoSession := sharedConfig.SSOSession
if ssoSession == nil {
// Fallback to legacy SSO session config parameters, if the
// sso-session section wasn't used.
ssoSession = &SSOSession{
Name: sharedConfig.SSOStartURL,
SSORegion: sharedConfig.SSORegion,
SSOStartURL: sharedConfig.SSOStartURL,
}
}
isaiahvita marked this conversation as resolved.
Show resolved Hide resolved

if sharedConfig.SSOSession != nil {
provider, err = resolveBearerAuthSSOTokenProvider(
ctx, cfg, ssoSession, configs)
ctx, cfg, sharedConfig.SSOSession, configs)
}

if err == nil && provider != nil {
Expand Down
2 changes: 1 addition & 1 deletion config/resolve_bearer_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestResolveBearerAuthToken(t *testing.T) {
SSOStartURL: "https://example.aws/start",
},
},
expectProvider: true,
expectProvider: false,
expectToken: smithybearer.Token{
Value: "access token",
CanExpire: true,
Expand Down
26 changes: 25 additions & 1 deletion config/resolve_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/sso"
"github.com/aws/aws-sdk-go-v2/service/ssooidc"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

Expand Down Expand Up @@ -171,7 +172,30 @@ func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *S
}

cfgCopy := cfg.Copy()
cfgCopy.Region = sharedConfig.SSORegion

if sharedConfig.SSOSession != nil {
ssoTokenProviderOptionsFn, found, err := getSSOTokenProviderOptions(ctx, configs)
if err != nil {
return fmt.Errorf("failed to get SSOTokenProviderOptions from config sources, %w", err)
}
var optFns []func(*ssocreds.SSOTokenProviderOptions)
if found {
optFns = append(optFns, ssoTokenProviderOptionsFn)
}
Comment on lines +181 to +184
Copy link
Contributor

Choose a reason for hiding this comment

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

optFns does not seem to be used anywhere in this function.

Copy link
Contributor

Choose a reason for hiding this comment

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

looks like this should of bee used as input for ssoidc.NewFromConfig?

Copy link
Member

Choose a reason for hiding this comment

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

I believe these are function options to be passed to the token provider constructor function actually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a good catch. Because optFns are *ssocreds.SSOTokenProviderOptions, it cannot be passed into the ssooidc constructor and must be passed into the token provider constructor. This means that instead of adding a new field to the sso Options and passing the ssoidc client as an option into the sso constructor:

		options = append(options, func(o *ssocreds.Options) {
			o.TokenClient = oidcClient
			o.CachedTokenFilepath = cachedPath
		})

I must make the added field a *SSOTokenProvider and pass that as an option into the sso constructor
Ill push a new commit to this PR with that change.

cfgCopy.Region = sharedConfig.SSOSession.SSORegion
cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name)
if err != nil {
return err
}
oidcClient := ssooidc.NewFromConfig(cfgCopy)
Comment on lines +186 to +190
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: put oidcClient := ssooidc.NewFromConfig(cfgCopy) at the top of this comment block to keep with other validation done for TokenClient

Suggested change
cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name)
if err != nil {
return err
}
oidcClient := ssooidc.NewFromConfig(cfgCopy)
oidcClient := ssooidc.NewFromConfig(cfgCopy)
cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name)
if err != nil {
return err
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm im a bit confused at this requested change.

this validation:

		cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name)
		if err != nil {
			return err
		}

isnt operating on any input related to oidcClient := ssooidc.NewFromConfig(cfgCopy)

additionally, the cached path conditional checks to see if the existing token is valid before creating an oidc client. if the existing token is not valid, then there is not reason to create an oidc client, so it should happen after

tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath, optFns...)
options = append(options, func(o *ssocreds.Options) {
o.SSOTokenProvider = tokenProvider
o.CachedTokenFilepath = cachedPath
})
} else {
cfgCopy.Region = sharedConfig.SSORegion
}

cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...)

Expand Down
8 changes: 8 additions & 0 deletions config/resolve_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,14 @@ func TestSharedConfigCredentialSource(t *testing.T) {
expectedSecretKey: "WEB_IDENTITY_SECRET",
expectedSessionToken: "WEB_IDENTITY_SESSION_TOKEN",
},
"SSO Session missing region": {
envProfile: "sso-session-missing-region",
expectedError: "profile \"sso-session-missing-region\" is configured to use SSO but is missing required configuration: sso_region",
},
"SSO Session mismatched region": {
envProfile: "sso-session-mismatched-region",
expectedError: "sso_region in profile \"sso-session-mismatched-region\" must match sso_region in sso-session",
},
"web identity": {
envProfile: "webident",
expectedAccessKey: "WEB_IDENTITY_AKID",
Expand Down
124 changes: 80 additions & 44 deletions config/shared_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,10 @@ type SSOSession struct {
SSOStartURL string
}

func (s *SSOSession) setFromIniSection(section ini.Section) error {
func (s *SSOSession) setFromIniSection(section ini.Section) {
updateString(&s.Name, section, ssoSessionNameKey)
updateString(&s.SSORegion, section, ssoRegionKey)
updateString(&s.SSOStartURL, section, ssoStartURLKey)

if s.SSORegion == "" || s.SSOStartURL == "" {
return fmt.Errorf(
"%v and %v are required parameters in sso-session section",
ssoRegionKey, ssoStartURLKey,
)
}

return nil
}

// SharedConfig represents the configuration fields of the SDK config files.
Expand Down Expand Up @@ -846,9 +838,8 @@ func (c *SharedConfig) setFromIniSections(profiles map[string]struct{}, profile
// profile only have credential provider options.
c.clearAssumeRoleOptions()
} else {
// First time a profile has been seen, It must either be a assume role
// credentials, or SSO. Assert if the credential type requires a role ARN,
// the ARN is also set, or validate that the SSO configuration is complete.
// First time a profile has been seen. Assert if the credential type
// requires a role ARN, the ARN is also set
if err := c.validateCredentialsConfig(profile); err != nil {
return err
}
Expand Down Expand Up @@ -900,31 +891,20 @@ func (c *SharedConfig) setFromIniSections(profiles map[string]struct{}, profile
// as a section in the config file. Load the SSO session using the name
// provided. If the session section is not found or incomplete an error
// will be returned.
if c.SSOSessionName != "" {
c.SSOSession, err = getSSOSession(c.SSOSessionName, sections, logger)
if err != nil {
return err
if c.hasSSOTokenProviderConfiguration() {
section, ok := sections.GetSection(ssoSectionPrefix + strings.TrimSpace(c.SSOSessionName))
if !ok {
return fmt.Errorf("failed to find SSO session section, %v", c.SSOSessionName)
}
var ssoSession SSOSession
ssoSession.setFromIniSection(section)
ssoSession.Name = c.SSOSessionName
c.SSOSession = &ssoSession
}

return nil
}

func getSSOSession(name string, sections ini.Sections, logger logging.Logger) (*SSOSession, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Make sure this function that was removed is not referenced anywhere else.

Copy link
Member

Choose a reason for hiding this comment

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

Since this is package private there is no concern for this being removed. If something else requires it within the package, then it wouldn't compile or pass tests.

section, ok := sections.GetSection(ssoSectionPrefix + strings.TrimSpace(name))
if !ok {
return nil, fmt.Errorf("failed to find SSO session section, %v", name)
}

var ssoSession SSOSession
if err := ssoSession.setFromIniSection(section); err != nil {
return nil, fmt.Errorf("failed to load SSO session %v, %w", name, err)
}
ssoSession.Name = name

return &ssoSession, nil
}

// setFromIniSection loads the configuration from the profile section defined in
// the provided INI file. A SharedConfig pointer type value is used so that
// multiple config file loadings can be chained.
Expand Down Expand Up @@ -1088,17 +1068,66 @@ func (c *SharedConfig) validateCredentialType() error {
len(c.CredentialProcess) != 0,
len(c.WebIdentityTokenFile) != 0,
) {
return fmt.Errorf("only one credential type may be specified per profile: source profile, credential source, credential process, web identity token, or sso")
return fmt.Errorf("only one credential type may be specified per profile: source profile, credential source, credential process, web identity token")
}

return nil
}

func (c *SharedConfig) validateSSOConfiguration() error {
Copy link
Contributor

Choose a reason for hiding this comment

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

note: validation in general has become stricter for both the legacy and current format.

if !c.hasSSOConfiguration() {
if c.hasSSOTokenProviderConfiguration() {
err := c.validateSSOTokenProviderConfiguration()
if err != nil {
return err
}
return nil
}

if c.hasLegacySSOConfiguration() {
err := c.validateLegacySSOConfiguration()
if err != nil {
return err
}
}
return nil
Comment on lines +1078 to +1092
Copy link
Member

Choose a reason for hiding this comment

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

Can both exist within the same profile, and if so is there precedence? As that would change how you would want this validation to work.

Choose a reason for hiding this comment

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

Yes - both could exist in one profile (its unlikely, but someone who is migrating from legacy to new could have such a setup). If so - the new format should be used, and the sso_start_url and sso_region MUST match (ie, if there is a mismatch between the profile's sso_region and the sso-session's, then we should raise an error).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@skmcgrail yes both can exist in the same profile. i intentionally check to see if the new configuration is present and valid. if it is valid, then it should be used and if it is not valid, then it should error (even if the legacy configuration is present and valid). is there another scenario in mind you might be thinking about thats not covered here?

also, regarding @alextwoods comment about matching sso_start_url and sso_region, i do that additional check here https://github.com/aws/aws-sdk-go-v2/pull/1903/files#diff-ad732dd8295a0ce04a495056007062e13a140b7f97f173004d0d6523545805d2R1120

}

func (c *SharedConfig) validateSSOTokenProviderConfiguration() error {
var missing []string

if len(c.SSOSessionName) == 0 {
missing = append(missing, ssoSessionNameKey)
}

if c.SSOSession == nil {
missing = append(missing, ssoSectionPrefix)
} else {
if len(c.SSOSession.SSORegion) == 0 {
missing = append(missing, ssoRegionKey)
}

if len(c.SSOSession.SSOStartURL) == 0 {
missing = append(missing, ssoStartURLKey)
}
}

if len(missing) > 0 {
return fmt.Errorf("profile %q is configured to use SSO but is missing required configuration: %s",
c.Profile, strings.Join(missing, ", "))
}

if len(c.SSORegion) > 0 && c.SSORegion != c.SSOSession.SSORegion {
return fmt.Errorf("%s in profile %q must match %s in %s", ssoRegionKey, c.Profile, ssoRegionKey, ssoSectionPrefix)
}

if len(c.SSOStartURL) > 0 && c.SSOStartURL != c.SSOSession.SSOStartURL {
return fmt.Errorf("%s in profile %q must match %s in %s", ssoStartURLKey, c.Profile, ssoStartURLKey, ssoSectionPrefix)
}

return nil
}

func (c *SharedConfig) validateLegacySSOConfiguration() error {
var missing []string

if len(c.SSORegion) == 0 {
Expand All @@ -1109,11 +1138,18 @@ func (c *SharedConfig) validateSSOConfiguration() error {
missing = append(missing, ssoStartURLKey)
}

if len(c.SSOAccountID) == 0 {
missing = append(missing, ssoAccountIDKey)
}

if len(c.SSORoleName) == 0 {
missing = append(missing, ssoRoleNameKey)
}

if len(missing) > 0 {
return fmt.Errorf("profile %q is configured to use SSO but is missing required configuration: %s",
c.Profile, strings.Join(missing, ", "))
}

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add back newline

return nil
}

Expand All @@ -1133,15 +1169,15 @@ func (c *SharedConfig) hasCredentials() bool {
}

func (c *SharedConfig) hasSSOConfiguration() bool {
switch {
case len(c.SSOAccountID) != 0:
case len(c.SSORegion) != 0:
case len(c.SSORoleName) != 0:
case len(c.SSOStartURL) != 0:
default:
return false
}
return true
return c.hasSSOTokenProviderConfiguration() || c.hasLegacySSOConfiguration()
}

func (c *SharedConfig) hasSSOTokenProviderConfiguration() bool {
return len(c.SSOSessionName) > 0
}

func (c *SharedConfig) hasLegacySSOConfiguration() bool {
return len(c.SSORegion) > 0 || len(c.SSOAccountID) > 0 || len(c.SSOStartURL) > 0 || len(c.SSORoleName) > 0
}

func (c *SharedConfig) clearAssumeRoleOptions() {
Expand Down
16 changes: 16 additions & 0 deletions config/shared_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,22 @@ func TestNewSharedConfig(t *testing.T) {
CredentialProcess: "/path/to/process",
},
},
"SSO Session success": {
ConfigFilenames: []string{testConfigFilename},
Profile: "sso-session-success",
Expected: SharedConfig{
Profile: "sso-session-success",
Region: "us-east-1",
SSOAccountID: "123456789012",
SSORoleName: "testRole",
SSOSessionName: "sso-session-success-dev",
SSOSession: &SSOSession{
Name: "sso-session-success-dev",
SSORegion: "us-east-1",
SSOStartURL: "https://d-123456789a.awsapps.com/start",
},
},
},
"profile names are case-sensitive (Mixed)": {
ConfigFilenames: []string{testConfigFilename},
CredentialsFilenames: []string{testCredentialsFilename},
Expand Down
19 changes: 19 additions & 0 deletions config/testdata/config_source_shared
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ sso_region = us-west-2
sso_role_name = TestRole
sso_start_url = https://127.0.0.1/start

[profile sso-session-missing-region]
region = us-east-1
sso_session = sso-session-missing-region-dev
sso_account_id = 123456789012
sso_role_name = testRole

[sso-session sso-session-missing-region-dev]
sso_start_url = https://d-123456789a.awsapps.com/start

[profile sso-session-mismatched-region]
sso_session = sso-session-mismatched-region-dev
sso_region = us-east-1
sso_account_id = 123456789012
sso_role_name = testRole

[sso-session sso-session-mismatched-region-dev]
sso_start_url = https://d-123456789a.awsapps.com/start
sso_region = us-west-2

[profile webident]
web_identity_token_file = ./testdata/wit.txt
role_arn = webident_arn
11 changes: 11 additions & 0 deletions config/testdata/shared_config
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ sso_role_name = TestRole
sso_start_url = https://127.0.0.1/start
credential_process = /path/to/process

[profile sso-session-success]
region = us-east-1
sso_session = sso-session-success-dev
sso_account_id = 123456789012
sso_role_name = testRole

[sso-session sso-session-success-dev]
sso_region = us-east-1
sso_start_url = https://d-123456789a.awsapps.com/start
sso_registration_scopes = sso:account:access

[profile DoNotNormalize]
aws_access_key_id = DoNotNormalize_config_akid
aws_secret_access_key = DoNotNormalize_config_secret
Expand Down