How token revocation works in Cognito

(Read this article on the blog) Tokens in Cognito When a user signs in to a user pool, Cognito generates 3 tokens: a refresh_token, an access_token, and an id_token. The access_token is used to make calls to the backend, and the refresh_token is a long-lived (depending on the app client settings) token to generate new […]

(Read this article on the blog)

Tokens in Cognito

When a user signs in to a user pool, Cognito generates 3 tokens: a refresh_token, an access_token, and an id_token. The access_token is
used to make calls to the backend, and the refresh_token is a long-lived (depending on the app client settings) token to generate new access_tokens.

userCognito LOGINrefresh_tokenaccess_tokenCognito TOKENaccess_tokenCognito TOKENaccess_tokencoderefresh_tokenrefresh_tokenRefresh token

refresh_tokenvalidaccess_tokenvalidaccess_tokenvalidaccess_tokenvalidTOKENTOKEN

These tokens contain all information required to use them and they are valid until they are expired. But what happens when the user logs out? Or when you
suspect an attacker got them and want to invalidate them?

This is a common problem with JWTs (JSON Web Token). The main appeal of them is that they are cryptographically signed so the receiver (the backend API, for
example) can verify them without contacting a separate stateful system. But that’s also their greatest weakness: when you know a token should not be used, how
do you prevent that?

Cognito offers a way to revoke a refresh_token and also to invalidate access_tokens. But it doesn’t magically solve the token invalidation problem.
In this article, we’ll look into how revocation works and what are the tradeoffs.

Revocation

To revoke a refresh_token, send it to the REVOCATION endpoint:

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

Cognito invalidates the token and all access_tokens that were generated with this refresh_token.

Cognito REVOKErefresh_tokenaccess_tokenaccess_tokenaccess_tokenrefresh_tokenRevoking a refresh_token invalidates it and all access_tokens

Revoken tokens

But what does it mean to revoke a token?

For a refresh_token, that means you won’t be able to generate new access_tokens with it. This works because refreshing needs a request to the TOKEN
endpoint and Cognito checks the status before generating new tokens.

Because of this, after a refresh_token is revoken it is not usable in any ways.

Access tokens

But access_tokens work differently. You pass them to the backend usually in the Authorization header and the backend usually does not check the
status of the token, just the information stored in it. For example, the AWS documentation
page
details how to confirm the
structure, validate the signature, verify the expiration, the audience, the issuer, and the claims, but the instructions do not include a revocation check.

You can implement revocation checking on your backend by sending the token to the USERINFO
endpoint
and see if Cognito returns a success response:

const openIdRes = await fetch(openIdConfig.openIdJson.userinfo_endpoint, {
	headers: new fetch.Headers({"Authorization": `Bearer ${auth_token}`}),
});
if (!openIdRes.ok) {
	throw new Error(JSON.stringify(await openIdRes.json()));
}

But this requires a network call and that invalidates all the benefits of the self-sufficient tokens. While it ensures that revocation is instantaneous, it
increases the duration and the cost of all API calls.

The easier way is to use a short expiration time for the access_token and just wait until the token expires. This works as when the refresh_token is
revoken it can’t be used to generate new access_tokens. So the maximum duration a revoken access_token is valid is the expiration time set for the
app client. If you can set that to an acceptable level then it’s a good tradeoff.

Conclusion

Cognito allows refresh_token revocation and that prevents generating new access_tokens with them. This also invalidates the existing
access_tokens, but for that to have any effect the backend needs to check revocation status for every API call. This is usually not done, so the best
practice is to use short expiration times and accept the tradeoff.

Source: Advanced Web Machinery