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:
- You call
aws sso login
and go through a browser authentication - You use your tools normally
- 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:
- A browser-based authorization occurs
- 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?
-
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-runaws sso login
. -
Read
.accessToken
from this file -
Use this access token, along with
{account-id}
and{role-name}
, to callGetRoleCredentials
-
Receive the usual triple of credentials and use them, until they expire
-
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:
-
list-accounts
: list the Accounts the user has access to -
list-account-roles
: list the Roles the user has access to in a given Account
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.