How token revocation works in Cognito
Refresh and access tokens can be invalidated but that might not prevent using them
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_token
s.
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_token
s. 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_token
s 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_token
s 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_token
s 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_token
s. 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_token
s with them. This also invalidates the existing
access_token
s, 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.