Skip to content

assume fails with 401 when unrelated SSO token exists in plaintext cache #940

@salimesmailjee

Description

@salimesmailjee

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

  1. 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.
  2. Ensure no valid token exists in the keychain (granted sso-tokens clear --all)
  3. 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):

  1. Filter plaintext tokens by scopeGetValidSSOTokenFromPlaintextCache 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:*).

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions