DEV Community

Using AWS Identity Center (SSO) tokens to script across multiple accounts

Short version: AWS Identity Center (formerly AWS SSO) stores a short-lived accessToken in a local JSON cache after you run aws sso login. You can exchange that token for per-account IAM temporary credentials using the sso.get_role_credentials API and then use those credentials with boto3.Session to run operations across multiple accounts. This post explains how the token flow works, security implications (yes — plain text cache), and gives a hands-on Python example you can adapt.


Table of contents

  1. How Identity Center tokens work (high level)
  2. Token → IAM credentials: what happens under the hood
  3. Security considerations
  4. What an attacker can do with a stolen accessToken
  5. Full example: running a ReadOnly check in all your SSO accounts using AWSReadOnlyAccess
  6. Hardening recommendations and alternatives

1) How Identity Center tokens work (high level)

When you run aws sso login the CLI performs an OIDC/OAuth flow and writes a small JSON token file to the SSO cache directory (usually ~/.aws/sso/cache/). The JSON contains an accessToken (a Bearer token) and an expiresAt timestamp.

That accessToken represents your authenticated Identity Center session (your user + any required MFA). It is not long-lived — the token has an expiry (1-12h, depending on your org's config).

You cannot directly use that accessToken like AWS credentials; instead you exchange it for IAM credentials for a specific account and role via the Identity Center sso API.

2) Token → IAM credentials: what happens under the hood

The key API is sso.get_role_credentials (available in boto3). You call it with:

  • accountId — the target AWS account you want access to
  • roleName — the Identity Center role name assigned in that account (for example, AWSReadOnlyAccess). You can also get available roles from sso.list_account_roles.
  • accessToken — the cached token from ~/.aws/sso/cache/*.json

The get_role_credentials call returns a short-lived set of credentials: accessKeyId, secretAccessKey, and sessionToken. These are standard STS-style credentials and you use them to create a boto3.Session that will act in the target account with the permissions associated to the role.

3) Security considerations

Important: the JSON files under ~/.aws/sso/cache/ are plain text JSON. The accessToken inside them is effectively a bearer token that can be exchanged for AWS credentials until it expires.

That means:

  • Treat the accessToken like a password / secret bearer token.
  • Protect the ~/.aws/sso/cache directory with strict file permissions (e.g., chmod 600).
  • Monitor and rotate your sessions: use the shortest practical session duration configured in your Identity Center settings.

4) What an attacker can do with a stolen accessToken

If a bad actor gains access to your SSO accessToken file, they can effectively impersonate you in AWS until that token expires. Here’s what happens:

  • The attacker can use your accessToken on their own computer to call AWS SSO APIs (ListAccounts, ListAccountRoles, and GetRoleCredentials).
  • Using GetRoleCredentials, they can obtain temporary IAM credentials for every account and role you have access to through Identity Center.
  • Those temporary credentials give them the same level of access that your assigned roles do — including read or write permissions, depending on your configuration.
  • Once they have those IAM keys, they can use standard AWS APIs (e.g., S3, EC2, IAM) from anywhere, even outside your corporate network.

This is why the SSO token must be treated like a highly sensitive credential — it’s effectively a universal key for your Identity Center session.

Even though the keys are short-lived, this can still result in data exfiltration, resource modification, or privilege escalation depending on your roles.

5) Full example: running a ReadOnly check in all your SSO accounts using AWSReadOnlyAccess

This example hardcodes the role name AWSReadOnlyAccess. It lists account IDs via SSO, then requests role credentials for AWSReadOnlyAccess in each account and performs an sts.get_caller_identity() check.

#!/usr/bin/env python3
"""Run a ReadOnly boto3 action in all AWS SSO accounts using a fixed role name"""
import json
import glob
import os
from datetime import datetime, timezone
import boto3
from botocore.exceptions import ClientError

AWS_SSO_CACHE_DIR = os.path.expanduser("~/.aws/sso/cache")
FIXED_ROLE_NAME = "AWSReadOnlyAccess"  # fixed role name

SSO_CLIENT = boto3.client("sso")

def find_latest_sso_token():
    files = glob.glob(os.path.join(AWS_SSO_CACHE_DIR, "*.json"))
    if not files:
        raise FileNotFoundError(f"No SSO cache JSON files found in {AWS_SSO_CACHE_DIR}")
    latest = max(files, key=os.path.getmtime)
    with open(latest, "r") as fh:
        data = json.load(fh)
    token = data.get("accessToken")
    expires_at = data.get("expiresAt")
    exp_dt = datetime.fromisoformat(expires_at.replace('Z', '+00:00')) if expires_at else None
    return token, exp_dt

def get_role_credentials(account_id, role_name, access_token):
    resp = SSO_CLIENT.get_role_credentials(
        accountId=account_id,
        roleName=role_name,
        accessToken=access_token,
    )
    creds = resp.get("roleCredentials")
    if not creds:
        raise RuntimeError(f"No roleCredentials for account {account_id}")
    return {
        "aws_access_key_id": creds["accessKeyId"],
        "aws_secret_access_key": creds["secretAccessKey"],
        "aws_session_token": creds["sessionToken"],
    }

def list_sso_account_ids(access_token):
    account_ids = []
    paginator = SSO_CLIENT.get_paginator("list_accounts")
    for page in paginator.paginate(accessToken=access_token):
        for acct in page.get("accountList", []):
            account_ids.append(acct["accountId"])
    return account_ids

def main():
    token, exp = find_latest_sso_token()
    if exp and exp <= datetime.now(timezone.utc):
        raise SystemExit("SSO token expired — run `aws sso login` again.")

    account_ids = list_sso_account_ids(token)

    if not account_ids:
        print("No accounts discovered for this SSO session.")
        return

    for acct_id in account_ids:
        try:
            creds = get_role_credentials(acct_id, FIXED_ROLE_NAME, token)
            sess = boto3.Session(**creds, region_name="eu-west-1")
            sts = sess.client("sts")
            who = sts.get_caller_identity()
            print(f"\n--> Account {acct_id}, caller identity:", {who['Arn']})
        except ClientError as e:
            print("  failed to get role credentials or call STS:", e)
        except RuntimeError as e:
            # Could indicate the role isn't assigned to you in that account
            print("  runtime error:", e)

if __name__ == "__main__":
    main()

Enter fullscreen mode Exit fullscreen mode

Notes:

  • If AWSReadOnlyAccess is not assigned to your Identity Center principal in a particular account, get_role_credentials will fail for that account.

6) Hardening recommendations and alternatives

  1. File permissions: make sure ~/.aws/sso/cache is only readable by your user (chmod 700 ~/.aws/sso/cache && chmod 600 ~/.aws/sso/cache/*.json).
  2. Avoid long sessions: Keep AWS Identity Center session duration as small as practical.
  3. Audit & monitor: enable CloudTrail and monitor GetRoleCredentials/AssumeRole patterns for suspicious usage.
  4. Least privilege: maybe you don’t need that AWSAdministratorAccess role in every account — start with AWSReadOnlyAccess and escalate only when necessary.

Top comments (0)