How to invalidate Cognito-issued JWT tokens

Yan Cui

I help clients go faster for less using serverless technologies.

The ability to invalidate a user’s session with immediate effect is a common enterprise requirement. For example:

  • If a user’s credentials are compromised, we need to immediately revoke the user’s access and force the user to change credentials.
  • If an employee is terminated or an external contractor’s access is revoked, their session should be invalidated immediately to prevent misuse.
  • Many regulations mandate strict access controls and the ability to prevent unauthorized access in real time.

However, this goes against how token-based authentication is designed to work. JWT tokens are stateless and are typically short-lived (for security reasons) but can be refreshed with refresh tokens.

So, is it possible to invalidate Cognito-issued JWT tokens?

The short answer is no.

The long answer is yes, you can achieve this effect with some work and some performance overhead.

Use short token validity

Cognito has a GlobalSignOut [1] and an AdminUserGlobalSignOut [2] API. These APIs invalidate a user’s ID, access and refresh tokens, and Cognito will no longer accept the invalidated tokens.

However, in token-based systems, the token contains the user’s claims and is cryptographically signed by the Identity Provider (IdP).

The service that receives the token doesn’t need to call the IdP to validate it—it just needs to check the signature. So, the IdP has no way of telling the service that the tokens have been invalidated.

As such, when you use a Cognito Authorizer with API Gateway or AppSync, the authorizer doesn’t know when a user’s tokens have been invalidated. Which is why you can’t invalidate Cognito-issued JWT tokens.

Instead, you can limit how long the JWT tokens remain valid by:

  1. Set the validity of the ID and access tokens to 5 minutes (the minimum allowed in Cognito).
  2. Use the GlobalSignOut or AdminUserGlobalSignOut API to log the user out and invalidate the user’s refresh token.
  3. When the front end attempts to refresh the JWT tokens, Cognito rejects the refresh toke, forcing the user to log in again.

This is the best you can do when you use the built-in Cognito Authorizer with API Gateway or AppSync.

Enforce token invalidation in real time

You can implement token invalidation if you:

  • use a Lambda authorizer with API Gateway or AppSync, or
  • perform authn & authz in your own application, e.g. when using Lambda Function URLs.

This repo [3] demonstrates an approach for implementing this.

In this solution, we used a Lambda authorizer with API Gateway. The authorizer has the TTL cache disabled to allow us to reject invalidated tokens immediately.

The authorizer uses the aws-jwt-verify [4] library to verify the auth token. To mark a token as invalidated, we set a tombstone for the token in Momento [5].

You can replace Momento with DynamoDB. However, I opted for Momento to ensure a fast (sub-millisecond) lookup, which is important because it needs to be done on every request.

When a user is signed out, we set the tombstone record for its token.

const response = await cacheClient.set(
  process.env.MOMENTO_CACHE_NAME, 
  `invalidated#${token}`,
  new Date().toJSON(),
  { 
    // 7 days, must be greater than the validity of the token
    ttl: 7 * 24 * 60 * 60, 
  }
);

On subsequent API calls, we check whether the tombstone record exists. If so, we know the token has been invalidated.

const response = await cacheClient.get(
  process.env.MOMENTO_CACHE_NAME,
  `invalidated#${token}`
);

switch (response.type) {
  case CacheGetResponse.Miss:
    console.log('Token is not invalidated');
    return false;
  case CacheGetResponse.Hit:
    console.log(`Token was invalidated at ${response.valueString()}`);
    return true;
}

Here’s an example flow:

Regarding performance overhead, the Lambda authorizer must run on every request.

On a cold start, the invocation (not accounting for init duration) takes 150-200ms. Most of which is attributed to fetching the JWKS from Cognito. You can optimize this by downloading the JWKS for your user pool and loading it from file, like this [6].

On a warm start, the invocation takes ~10-15ms.

There are also additional costs to consider:

  • The Lambda authorizer response cannot be cached. So, you will pay for a Lambda invocation for every API request.
  • The Lambda authorizer needs to look up the tombstone record on every request, so you must account for the cost of these read operations.

Summary

Invalidating Cognito-issued JWT tokens isn’t natively supported. But you can mitigate risks by setting short token validity and using the GlobalSignOut APIs to stop new tokens from being issued.

For stricter requirements, a Lambda authorizer with a fast datastore (e.g. Momento) can reject invalidated tokens in real time.

Whilst it’s not a difficult solution to implement, you must weigh the cost and performance overhead of this approach against the security and compliance it provides. 

If the choice isn’t clear, or you need help implementing a similar solution, remember, I’m ?just an email away?.

Links

[1] GlobalSignOut API

[2] AdminUserGlobalSignOut API

[3] Demo repo for how to invalidate Cognito-issued tokens

[4] aws-jwt-verify library for verifying Cognito-issued tokens

[5] Momento

[6] How to load JWKS from file for aws-jwt-verify

Whenever you’re ready, here are 3 ways I can help you:

  1. Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
  2. I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
  3. Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.