How token revocation works in Cognito

Refresh and access tokens can be invalidated but that might not prevent using them

Author's image
Tamás Sallai
4 mins

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.

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.

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.

September 21, 2021
In this article