How to use the refresh token with Cognito

(Read this article on the blog) Cognito tokens When a client logs in to a Cognito user pool they get 3 tokens: a refresh_token, an id_token, and an access_token. Later, when the client makes requests to the backend it attaches the access_token to the request. The backend API then checks the token and verify that […]

(Read this article on the blog)

Cognito tokens

When a client logs in to a Cognito user pool they get 3 tokens: a refresh_token, an id_token, and an access_token. Later, when the client makes
requests to the backend it attaches the access_token to the request. The backend API then checks the token and verify that it’s valid, it’s not tampered
with, and it’s generated by the expected Cognito app client.

When using the authorization code flow (the recommended one for SPAs), the tokens are returned by the TOKEN
endpoint after exchanging the code for them:

const res = await fetch(`${cognitoLoginUrl}/oauth2/token`, {
	method: "POST",
	headers: new Headers({"content-type": "application/x-www-form-urlencoded"}),
	body: Object.entries({
		"grant_type": "authorization_code",
		"client_id": clientId,
		"code": searchParams.get("code"),
		"code_verifier": codeVerifier,
		"redirect_uri": window.location.origin,
	}).map(([k, v]) => `${k}=${v}`).join("&"),
});
if (!res.ok) {
	throw new Error(await res.json());
}

const tokens = await res.json();

/*
tokens = {
	access_token: "..."
	expires_in: 3600
	id_token: "..."
	refresh_token: "..."
	token_type: "Bearer"
}
*/

For the backend requests, the access_token is sent in the Authorization header:

const apiRes = await fetch("/api/user", {
	headers: new Headers({"Authorization": `Bearer ${tokens.access_token}`}),
});

Token expiration times

The three tokens are usable for different durations. You can configure these for the Cognito app client:

The access_token and the id_token are short-lived. You can not set them to be valid for more than 1 day and the default is 60 minutes.

The refresh_token is long-lived. It can be valid for up to 10 years, and the default is 30 days.

Refreshing an access token

Since the access_token expires regularly as the users interact with the backend, the client needs to generate new ones. This is what the
refresh_token is good for.

Using the refresh_token is a call to the same TOKEN endpoint the authorization code uses but the grant_type is refresh_token:

const res = await fetch(`${cognitoLoginUrl}/oauth2/token`, {
	method: "POST",
	headers: new Headers({"content-type": "application/x-www-form-urlencoded"}),
	body: Object.entries({
		"grant_type": "refresh_token",
		"client_id": clientId,
		"redirect_uri": window.location.origin,
		"refresh_token": tokens.refresh_token,
	}).map(([k, v]) => `${k}=${v}`).join("&"),
});
if (!res.ok) {
	throw new Error(await res.json());
}
const newTokens = await res.json();

/*
newTokens = {
	access_token: "..."
	expires_in: 3600
	id_token: "..."
	token_type: "Bearer"
}
*/

The result does not include a refresh_token, only an access_token and an id_token. Because of this, the client needs to relogin to get a new
refresh_token when it expires.

Token expiration timing

Why this complication with the refresh_token then? Why not Cognito returns just one token that is valid for the full duration of the client session?

First, you might store the refresh_token in a different place. For example, you can implement a backend endpoint that stores it and generates
access_tokens for the client when it needs them. This way, the refresh_token won’t be stored in the browser.

Second, refresh_tokens and access_tokens can be revoked. But checking an access_token if it’s revoken or not for every API call is slow and
expensive as that requires an extra network call. On the other hand, if you use short expiration times for the access_tokens then they will be invalid
after revocation without an explicit check. And there is a validity check implemented by Cognito when the refresh_token is used, so a revoken token won’t
be able to generate new access_tokens.

Considerations

So, what’s a good value for the token validity settings?

As usual, it’s about tradeoffs. Let’s see what the default of 60 minutes and 30 days mean!

  • The client needs to refresh the token every hour (this is an automated process)
  • A revoken token is valid for up to 60 minutes
  • The user must relogin every month (this is a manual process)

If you want the user session to last longer, increase the validity of the refresh_token.

And if you want token revocation to have a more immediate effect, you can either reduce the lifespan of the access_token or implement per-call
revocation checking.

Conclusion

Cognito returns a refresh_token when a user signs in along with an access_token and an id_token. It is a longer-lived token with that the
client can use to generate new access_tokens and id_tokens.

Source: Advanced Web Machinery