Skip to content

Commit

Permalink
Refresh SSO AccessToken if first attempt errors out
Browse files Browse the repository at this point in the history
For some reason our SSO AccessTokens are being marked as invalid
by AWS SSO API and so the sso.GetAccounts() and sso.GetRoles()
calls should try force reauthenticating once to AWS SSO in order to get a
new AccessToken for these calls.

Refs: #279
  • Loading branch information
synfinatic committed Feb 5, 2022
1 parent 2b92afd commit 982b829
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 29 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# AWS SSO CLI Changelog

## [v1.8.0] - Unreleased
## [v1.7.2] - Unreleased

### Bug Fixes

* Cached AWS SSO AccessToken is sometimes invalid even though it was not expired
and any calls to SSO were failing. #279

### Changes

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PROJECT_VERSION := 1.7.1
PROJECT_VERSION := 1.7.2
DOCKER_REPO := synfinatic
PROJECT_NAME := aws-sso

Expand Down
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func doAuth(ctx *RunContext) *sso.AWSSSO {
log.Fatalf("%s", err.Error())
}
AwsSSO = sso.NewAWSSSO(s, &ctx.Store)
err = AwsSSO.Authenticate(ctx.Settings.UrlAction, ctx.Settings.Browser)
err = AwsSSO.Authenticate(ctx.Settings.UrlAction, ctx.Settings.Browser, false)
if err != nil {
log.WithError(err).Fatalf("Unable to authenticate")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/tags_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (cc *TagsCmd) Run(ctx *RunContext) error {
if ctx.Cli.Tags.ForceUpdate {
s := set.SSO[ctx.Cli.SSO]
awssso := sso.NewAWSSSO(s, &ctx.Store)
err := awssso.Authenticate(ctx.Settings.UrlAction, ctx.Settings.Browser)
err := awssso.Authenticate(ctx.Settings.UrlAction, ctx.Settings.Browser, false)
if err != nil {
log.WithError(err).Fatalf("Unable to authenticate")
}
Expand Down
81 changes: 57 additions & 24 deletions sso/awssso.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type AWSSSO struct {
Accounts []AccountInfo `json:"Accounts"`
Roles map[string][]RoleInfo `json:"Roles"`
SSOConfig *SSOConfig `json:"SSOConfig"`
urlAction string // cache for future calls
browser string // cache for future calls
}

func NewAWSSSO(s *SSOConfig, store *storage.SecureStorage) *AWSSSO {
Expand Down Expand Up @@ -83,26 +85,37 @@ func (as *AWSSSO) StoreKey() string {
return fmt.Sprintf("%s|%s", as.SsoRegion, as.StartUrl)
}

func (as *AWSSSO) Authenticate(urlAction, browser string) error {
func (as *AWSSSO) Authenticate(urlAction, browser string, force bool) error {
if urlAction != "" {
as.urlAction = urlAction
}

if browser != "" {
as.browser = browser
}

// see if we have valid cached data
token := storage.CreateTokenResponse{}
err := as.store.GetCreateTokenResponse(as.StoreKey(), &token)
if err == nil && !token.Expired() {
as.Token = token
return nil
} else if err != nil {
log.Debugf(err.Error())
} else {
if as.Token.ExpiresAt != 0 {
t := time.Unix(as.Token.ExpiresAt, 0)
log.Infof("Cached SSO token expired at: %s. Reauthenticating...\n", t.Format("Mon Jan 2 15:04:05 -0700 MST 2006"))
if !force {
token := storage.CreateTokenResponse{}
err := as.store.GetCreateTokenResponse(as.StoreKey(), &token)
if err == nil && !token.Expired() {
as.Token = token
return nil
} else if err != nil {
log.Debugf(err.Error())
} else {
log.Infof("Cached SSO token has expired. Reauthenticating...\n")
if as.Token.ExpiresAt != 0 {
t := time.Unix(as.Token.ExpiresAt, 0)
log.Infof("Cached SSO token expired at: %s. Reauthenticating...\n",
t.Format("Mon Jan 2 15:04:05 -0700 MST 2006"))
} else {
log.Infof("Cached SSO token has expired. Reauthenticating...\n")
}
}
}

// Nope- fall back to our standard process
err = as.RegisterClient()
err := as.RegisterClient()
if err != nil {
return fmt.Errorf("Unable to RegisterClient: %s", err.Error())
}
Expand All @@ -117,15 +130,15 @@ func (as *AWSSSO) Authenticate(urlAction, browser string) error {
return fmt.Errorf("Unable to get DeviceAuthInfo: %s", err.Error())
}

err = utils.HandleUrl(urlAction, browser, auth.VerificationUriComplete,
err = utils.HandleUrl(as.urlAction, as.browser, auth.VerificationUriComplete,
"Please open the following URL in your browser:\n\n", "\n\n")
if err != nil {
return err
}

log.Infof("Waiting for SSO authentication...")

err = as.CreateToken()
err = as.CreateToken(force)
if err != nil {
return fmt.Errorf("Unable to get AWS SSO Token: %s", err.Error())
}
Expand Down Expand Up @@ -225,12 +238,14 @@ func (as *AWSSSO) GetDeviceAuthInfo() (DeviceAuthInfo, error) {
}

// Blocks until we have a token
func (as *AWSSSO) CreateToken() error {
func (as *AWSSSO) CreateToken(force bool) error {
log.Tracef("CreateToken()")
err := as.store.GetCreateTokenResponse(as.StoreKey(), &as.Token)
if err == nil && !as.Token.Expired() {
log.Info("Using CreateToken cache")
return nil
if !force {
err := as.store.GetCreateTokenResponse(as.StoreKey(), &as.Token)
if err == nil && !as.Token.Expired() {
log.Info("Using CreateToken cache")
return nil
}
}

input := ssooidc.CreateTokenInput{
Expand All @@ -247,6 +262,7 @@ func (as *AWSSSO) CreateToken() error {
retryInterval = time.Duration(as.DeviceAuth.Interval) * time.Second
}

var err error
var resp *ssooidc.CreateTokenOutput

for {
Expand Down Expand Up @@ -274,7 +290,7 @@ func (as *AWSSSO) CreateToken() error {
AccessToken: aws.ToString(resp.AccessToken),
ExpiresIn: resp.ExpiresIn,
ExpiresAt: time.Now().Add(secs).Unix(),
IdToken: aws.ToString(resp.IdToken),
IdToken: aws.ToString(resp.IdToken), // per AWS docs, this may be undefined
RefreshToken: aws.ToString(resp.RefreshToken), // per AWS docs, not currently implemented
TokenType: aws.ToString(resp.TokenType),
}
Expand Down Expand Up @@ -325,7 +341,16 @@ func (as *AWSSSO) GetRoles(account AccountInfo) ([]RoleInfo, error) {
}
output, err := as.sso.ListAccountRoles(context.TODO(), &input)
if err != nil {
return as.Roles[account.AccountId], err
// sometimes our AccessToken is invalid even though it has not expired
// so retry once
log.Debugf("Unexpected AccessToken failure. Refreshing...")
if err = as.Authenticate("", "", true); err != nil {
return as.Roles[account.AccountId], err
}
input.AccessToken = aws.String(as.Token.AccessToken)
if output, err = as.sso.ListAccountRoles(context.TODO(), &input); err != nil {
return as.Roles[account.AccountId], err
}
}
for i, r := range output.RoleList {
var via string
Expand Down Expand Up @@ -401,7 +426,15 @@ func (as *AWSSSO) GetAccounts() ([]AccountInfo, error) {
}
output, err := as.sso.ListAccounts(context.TODO(), &input)
if err != nil {
return as.Accounts, err
// sometimes our AccessToken is invalid so try a new one once?
log.Debugf("Unexpected AccessToken failure. Refreshing...")
if err = as.Authenticate("", "", true); err != nil {
return as.Accounts, err
}
input.AccessToken = aws.String(as.Token.AccessToken)
if output, err = as.sso.ListAccounts(context.TODO(), &input); err != nil {
return as.Accounts, err
}
}
for i, r := range output.AccountList {
as.Accounts = append(as.Accounts, AccountInfo{
Expand Down
3 changes: 2 additions & 1 deletion sso/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (c *Cache) Save(updateTime bool) error {
}
jbytes, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("Unable to masrhal json: %s", err.Error())
return fmt.Errorf("Unable to marshal json: %s", err.Error())
}
err = utils.EnsureDirExists(c.CacheFile())
if err != nil {
Expand Down Expand Up @@ -377,6 +377,7 @@ func (c *Cache) NewRoles(as *AWSSSO, config *SSOConfig) (*Roles, error) {
return &r, nil
}

// addSSORoles retrieves all the SSO Roles from AWS SSO and places them in r
func (c *Cache) addSSORoles(r *Roles, as *AWSSSO) error {
cache := c.GetSSO()

Expand Down

0 comments on commit 982b829

Please sign in to comment.