From d380a00fb24c2f9ab8dabb5b72b8390a4c31d636 Mon Sep 17 00:00:00 2001 From: Salim Esmailjee <52280007+salimesmailjee@users.noreply.github.com> Date: Thu, 14 May 2026 11:25:55 +0100 Subject: [PATCH] fix: skip plaintext cache tokens with non-SSO scopes GetValidSSOTokenFromPlaintextCache matched tokens by startUrl only, causing it to pick up IDE extension tokens (e.g. codewhisperer:* scopes) that lack sso:account:access. This resulted in a 401 UnauthorizedException with no fallback to the browser login flow. Add scope filtering to skip tokens that contain only non-SSO scopes. Fixes #940 --- pkg/cfaws/ssotoken.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/pkg/cfaws/ssotoken.go b/pkg/cfaws/ssotoken.go index a68052ef..f4a6d4bb 100644 --- a/pkg/cfaws/ssotoken.go +++ b/pkg/cfaws/ssotoken.go @@ -16,11 +16,12 @@ import ( ) type SSOPlainTextOut struct { - AccessToken string `json:"accessToken"` - ExpiresAt string `json:"expiresAt"` - SSOSessionName string `json:"ssoSessionName"` - StartUrl string `json:"startUrl"` - Region string `json:"region"` + AccessToken string `json:"accessToken"` + ExpiresAt string `json:"expiresAt"` + SSOSessionName string `json:"ssoSessionName"` + StartUrl string `json:"startUrl"` + Region string `json:"region"` + Scopes []string `json:"scopes,omitempty"` } // CreatePlainTextSSO is currently unused. In a future version of the Granted CLI, @@ -182,8 +183,8 @@ func ReadPlaintextSsoCreds(startUrl string) (SSOPlainTextOut, error) { if err != nil { return SSOPlainTextOut{}, err } - // check if the startUrl matches - if sso.StartUrl == startUrl { + // check if the startUrl matches and the token has appropriate scopes + if sso.StartUrl == startUrl && hasSSOScopes(sso.Scopes) { return sso, nil } } @@ -192,6 +193,21 @@ func ReadPlaintextSsoCreds(startUrl string) (SSOPlainTextOut, error) { return SSOPlainTextOut{}, fmt.Errorf("no valid sso token found") } +// hasSSOScopes returns true if the token has scopes that include SSO account +// access, or if no scopes are specified (for backwards compatibility with +// tokens that don't include scope metadata). +func hasSSOScopes(scopes []string) bool { + if len(scopes) == 0 { + return true + } + for _, scope := range scopes { + if strings.HasPrefix(scope, "sso:") { + return true + } + } + return false +} + func GetValidSSOTokenFromPlaintextCache(startUrl string) *securestorage.SSOToken { if SsoCredsAreInConfigCache() { creds, err := ReadPlaintextSsoCreds(startUrl)