assume fails with 401 when unrelated SSO token exists in plaintext cache
Summary
assume picks up an unrelated SSO token from ~/.aws/sso/cache/ (written by another tool, e.g. AWS IDE Extensions for VS Code) because GetValidSSOTokenFromPlaintextCache matches tokens solely by startUrl without checking whether the token's scopes are appropriate for sso:account:access. This causes a 401 error with no fallback to the browser login flow.
Environment
- Granted version: 0.39.0
- OS: macOS (Apple Silicon)
- Shell: zsh
- Keyring backend: keychain
- Profile config style:
sso-session reference (not legacy)
AWS Config
[sso-session my-sso]
sso_start_url = https://my-org.awsapps.com/start
sso_region = eu-west-2
sso_registration_scopes = sso:account:access
[profile my-account/MyRole]
sso_session = my-sso
sso_account_id = 123456789012
sso_role_name = MyRole
region = eu-west-2
sso_auto_populated = true
Reproduction Steps
- Have an IDE extension (e.g. AWS Toolkit / Amazon Q for VS Code) authenticated against the same SSO start URL. This writes a token to
~/.aws/sso/cache/<hash>.json with scopes like codewhisperer:completions, codewhisperer:analysis, etc.
- Ensure no valid token exists in the keychain (
granted sso-tokens clear --all)
- Run
assume my-account/MyRole
Expected Behaviour
assume should detect that no valid token with sso:account:access scope exists and initiate the browser-based device authorization flow (calling idclogin.Login).
Actual Behaviour
assume finds the IDE extension's token in ~/.aws/sso/cache/ via GetValidSSOTokenFromPlaintextCache (which matches only on startUrl), stores it to the keychain, then uses it to call GetRoleCredentials. AWS returns a 401 UnauthorizedException because the token lacks the sso:account:access scope. Granted does not fall through to the browser login — it just exits with the error.
Verbose Output
[keyring] Querying keychain for service="granted-aws-sso-tokens", account="https://my-org.awsapps.com/startmy-sso", keychain="login.keychain"
[keyring] No results found
[!] error retrieving IAM Identity Center token from secure storage: The specified item could not be found in the keyring
[keyring] Adding service="granted-aws-sso-tokens", label="", account="https://my-org.awsapps.com/startmy-sso", trusted=true to osx keychain "login.keychain"
[keyring] Removing keychain item service="granted-aws-sso-tokens", account="https://my-org.awsapps.com/start", keychain "login.keychain"
[DEBUG] clearing sso token from the credentials cache: The specified item could not be found in the keyring
[✘] operation error SSO: GetRoleCredentials, https response error StatusCode: 401, RequestID: ..., UnauthorizedException: Session token not found or invalid
Root Cause
In pkg/cfaws/ssotoken.go, GetValidSSOTokenFromPlaintextCache matches cache files by startUrl only:
if sso.StartUrl == startUrl {
return sso, nil
}
It does not check whether the token's scopes include sso:account:access (or match the sso_registration_scopes from the sso-session config). Any non-expired token with the same startUrl is treated as valid for assuming roles.
Additionally, in pkg/cfaws/assumer_aws_sso.go, SSOLogin does not retry with the browser login flow when a plaintext-sourced token results in a 401. The 401 handling in SSOLoginWithToken only clears the cached token and returns the error — it doesn't loop back to trigger idclogin.Login.
Suggested Fix
Either (or both):
-
Filter plaintext tokens by scope — GetValidSSOTokenFromPlaintextCache should verify that the token's scopes include sso:account:access (or at minimum, don't match tokens that only have unrelated scopes like codewhisperer:*).
-
Retry on 401 — When SSOLoginWithToken receives a 401 UnauthorizedException and the token originated from the plaintext cache (not from a fresh login), SSOLogin should discard the token and fall through to idclogin.Login rather than propagating the error.
Workaround
Using assume --no-cache bypasses the plaintext cache lookup and forces a fresh login. However, this should not be required in normal usage.
assumefails with 401 when unrelated SSO token exists in plaintext cacheSummary
assumepicks up an unrelated SSO token from~/.aws/sso/cache/(written by another tool, e.g. AWS IDE Extensions for VS Code) becauseGetValidSSOTokenFromPlaintextCachematches tokens solely bystartUrlwithout checking whether the token's scopes are appropriate forsso:account:access. This causes a 401 error with no fallback to the browser login flow.Environment
sso-sessionreference (not legacy)AWS Config
Reproduction Steps
~/.aws/sso/cache/<hash>.jsonwith scopes likecodewhisperer:completions,codewhisperer:analysis, etc.granted sso-tokens clear --all)assume my-account/MyRoleExpected Behaviour
assumeshould detect that no valid token withsso:account:accessscope exists and initiate the browser-based device authorization flow (callingidclogin.Login).Actual Behaviour
assumefinds the IDE extension's token in~/.aws/sso/cache/viaGetValidSSOTokenFromPlaintextCache(which matches only onstartUrl), stores it to the keychain, then uses it to callGetRoleCredentials. AWS returns a 401UnauthorizedExceptionbecause the token lacks thesso:account:accessscope. Granted does not fall through to the browser login — it just exits with the error.Verbose Output
Root Cause
In
pkg/cfaws/ssotoken.go,GetValidSSOTokenFromPlaintextCachematches cache files bystartUrlonly:It does not check whether the token's scopes include
sso:account:access(or match thesso_registration_scopesfrom thesso-sessionconfig). Any non-expired token with the samestartUrlis treated as valid for assuming roles.Additionally, in
pkg/cfaws/assumer_aws_sso.go,SSOLogindoes not retry with the browser login flow when a plaintext-sourced token results in a 401. The 401 handling inSSOLoginWithTokenonly clears the cached token and returns the error — it doesn't loop back to triggeridclogin.Login.Suggested Fix
Either (or both):
Filter plaintext tokens by scope —
GetValidSSOTokenFromPlaintextCacheshould verify that the token's scopes includesso:account:access(or at minimum, don't match tokens that only have unrelated scopes likecodewhisperer:*).Retry on 401 — When
SSOLoginWithTokenreceives a 401UnauthorizedExceptionand the token originated from the plaintext cache (not from a fresh login),SSOLoginshould discard the token and fall through toidclogin.Loginrather than propagating the error.Workaround
Using
assume --no-cachebypasses the plaintext cache lookup and forces a fresh login. However, this should not be required in normal usage.