The ability to invalidate a user's session with immediate effect is a common enterprise requirement. For example:
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 validityCognito 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:
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 timeYou can implement token invalidation if you:
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. On subsequent API calls, we check whether the tombstone record exists. If so, we know the token has been invalidated. 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:
SummaryInvalidating 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 |
Join 15K readers and level up you AWS game with just 5 mins a week.
Last week, we looked at 6 ways to version event schemas [1] and found the best solution is to avoid breaking changes and minimise the need for versioning. But how exactly do you do that? How can you prevent accidental breaking changes from creeping in? You can detect and stop breaking changes: At runtime, when the events are ingested; During development, when schema changes are made; Or a combination of both! Here are three approaches you should consider. 1. Consumer-Driven Contracts In...
Synchronous API integrations create temporal coupling [1] between two services based on their respective availability. This is a tighter form of coupling and often necessitates techniques such as retries, exponential delay and fallbacks to compensate. Event-driven architectures, on the other hand, encourage loose coupling. But we are still bound by lessor forms of coupling such as schema coupling. And here lies a question that many students and clients have asked me: “How do I version my...
ICYMI, Serverless Inc. recently announced the Serverless Container Framework. It allows you to switch the compute platform between Lambda and Fargate with a one-liner config change. This is a game-changer for many organizations! It'd hopefully nullify many of the "lock-in" worries about Lambda, too. As your system grows, if Lambda gets expensive, you can easily switch to Fargate without changing your application code. To be clear, this is something you can already do yourself. It's not a...