Skip to the content.

AWS SSO Credentials

by @pbrisbin on September 16, 2022

At Freckle, we use AWS SSO to manage our operators’ CLI access. The process typically goes like this:

  1. You call aws sso login and go through a browser authentication
  2. You use your tools normally
  3. Eventually, that access expires and you aws sso login again

This works great for the aws CLI itself and anything built in a language like Python or JavaScript with well-supported SDKs. But what happens when your tool doesn’t have support for the kind of credentials the SSO process uses?

Third-party tools do exist in this space, but not all use-cases need that much power. In this post, I’ll explain what actually happens behind the scenes when any SDK handles SSO-based credentials, and show how to work with tools that lack direct support, by running that same process from the outside.

AWS SSO Login

Any AWS SSO user is going to have to configure a block in ~/.aws/config:

[profile {name}]
sso_start_url = {start-url}
sso_region = {region-used-for-sso}
sso_account_id = {account-id}
sso_role_name = {role-name}

When the user performs aws --profile {name} sso login, the following happens:

  1. A browser-based authorization occurs
  2. Access token data is written to a JSON file

This “Access token” is different than the credentials that are used to make actual API calls. It’s a JWT used to retrieve said credentials later.

The data is written to ~/.aws/sso/cache/{SHA1({start-url})}.json:

{
  "startUrl": "{start-url}",
  "region": "{region-used-for-sso}",
  "accessToken": "{token}",
  "expiresAt": "{date}"
}

Using SSO Credentials

If you’re familiar with the AWS CLI, you know there is a nuanced order of sources from which profile or credential information can be read. Any SDK will first decide if SSO is appropriate, which it usually does by looking for the presence of a {start-url}, {account-id}, and {role-name}.

Let’s assume it found those details through the named profile. What’s next?

  1. Look for ~/.aws/sso/cache/{SHA1({start-url})}.json

    If the file’s missing, or its .expiresAt is in the past, emit an error to the user that they need to re-run aws sso login.

  2. Read .accessToken from this file

  3. Use this access token, along with {account-id} and {role-name}, to call GetRoleCredentials

  4. Receive the usual triple of credentials and use them, until they expire

  5. Repeat.

Hacking SSO Support

So let’s say you have an app that doesn’t directly support this flow. In our case, this was Transmit, which some of our Curriculum folks use to manage S3 assets. Can you use SSO credentials with such an app?

Of course you can! The process above ended with “receive […] credentials and use them”, so we could handle the first steps ourselves and then hand those normal credentials to the app. And that’s exactly what we’ll do.

We’ll need to know {start-url}, {account-id}, and {role-name}. You could write some gnarly shell to read them from the ~/.aws/config file, but I’m just going to assume we are given them. They rarely change for any given use-case, and in ours we were managing the ~/.aws/config stanza on behalf of the users too, so we had to hard-code them anyway.

#!/usr/bin/env bash
set -euo pipefail

start_url=...
account_id=...
role_name=...

# Credentials will be written under this name
profile_name=...

Steps 1 and 2:

token_file=$HOME/.aws/sso/cache/$(echo -n "$start_url" | sha1sum | awk '{print $1}').json

if expires_at=$(jq --raw-output '.expiresAt' "$token_file"); then
  # A negative $((expires-now)) would mean expired
  diff=$(( "$(date -d "$expires_at" '+%s')" - "$(date '+%s')" ))
else
  # Treat other errors just like expired
  diff=-100
fi

if ((diff <= 0)); then
  echo "SSO Access Token not found or expired, please run \`aws sso login'" >&2
  exit 1
fi

if ! access_token=$(jq --raw-output '.accessToken' "$token_file"); then
  # It's unlikely above would succeed and this will fail, but
  echo "Unable to read access token from $token_file" >&2
  exit 1
fi

Steps 3 and 4:

aws sso get-role-credentials \
  --access-token "$access_token" \
  --account-id "$account_id" \
  --role-name "$role_name" |
  jq --raw-output '
    .roleCredentials |
      ( "['"$profile_name"']\n"
      + "access_key_id = " + .accessKeyId + "\n"
      + "secret_access_key = " + .secretAccessKey + "\n"
      + "session_token = " + .sessionToken + "\n"
      )
  ' >> ~/.aws/credentials

At this point, you can configure your tool to use the named profile and it’ll find and use these credentials.

Extensions & Caveats

The aws sso sub-command has two other useful commands:

With these, you could build up a little selection UI used to set {account-id} and {role-name} before launching into the above process. Or you could at least present more informative errors if the script is run with incorrect values (e.g. a Role you don’t have access to).

If your tool doesn’t support named profiles or reading ~/.aws/credentials, you’ll need to export the usual AWS_ variables:

eval "$(
  aws sso get-role-credentials \
    --access-token "$access_token" \
    --account-id "$account_id" \
    --role-name "$role_name" |
    jq --raw-output '
      .roleCredentials |
        ( "export AWS_ACCESS_KEY_ID=" + .accessKeyId + "\n"
        + "export AWS_SECRET_ACCESS_KEY=" + .secretAccessKey + "\n"
        + "export AWS_SESSION_TOKEN=" + .sessionToken + "\n"
        )
    '
)"

If the eval scares you, you could write the JSON to a file, and read and export individual values directly.

One of the big downsides of this script as written is that the ephemeral credentials are usually pretty short-lived. The SSO Access Token at Freckle expires about once a day, so I only need to run aws sso login that often. This part is unavoidable, but as long as that Access Token is valid, new short-lived credentials could be retrieved without any user intervention. Official SDKs usually launch a background thread to do exactly this. The script above clearly doesn’t, so it’ll need to be re-run frequently.

I thought about putting this into something like a systemd timer, to run on an interval and refresh the short-lived tokens as necessary, but that’s a little too much complexity for me. I chose instead to just go and build actual support into the upstream tool where this was impacting us, which is how I came to understand the surprisingly simple mechanics of it all to begin with.