FusionAuth’s event and Webhook system as well as the jwt.refresh-token.revoke
event. If you are building your own IdP or using another system, you might need to build out your own eventing system based on this article.
The FusionAuth jwt.refresh-token.revoke event looks like this:
{
"event": {
"type": "jwt.refresh-token.revoke",
"applicationTimeToLiveInSeconds": {
"cc0567da-68a1-45f3-b15b-5a6228bb7146": 600
},
"userId": "00000000-0000-0000-0000-000000000001"
}
}
Next, let’s write a simple Webhook in our application that will receive this event and update the JWTManager. (NOTE: our example has a variable called applicationId
that is a global variable that stores the Id of the application itself - in this case it would be cc0567da-68a1-45f3-b15b-5a6228bb7146). Our code below is written in Node.js and uses the FusionAuth Node client library.
/* Handle FusionAuth event. */
router.post('/fusionauth-webhook', function(req, res, next) {
JWTManager.revoke(req.body.event.userId, req.body.event.applicationTimeToLiveInSeconds[applicationId]);
res.sendStatus(200);
});
Here is how the JWTManager maintains the list of user Ids whose JWTs should be revoked. Our implementation also starts a thread to clean up after itself so we don’t run out of memory.
export class JWTManager {
public static revokedJWTs: object = {};
constructor() {}
/**
* Checks if a JWT is valid. This assumes that the JWT contains a property named <code>exp</code> that is a
* NumericDate value defined in the JWT specification and a property named <code>sub</code> that is the user Id the
* JWT belongs to.
*
* @param {object} jwt The JWT object.
* @returns {boolean} True if the JWT is valid, false if it isn't.
*/
public static isValid(jwt): boolean {
const expiration = JWTManager.revokedJWTs[jwt.sub];
return typeof(expiration) === 'undefined' || expiration === null || expiration < jwt.exp * 1000;
}
/**
* Revokes all JWTs for the user with the given Id using the duration (in seconds).
*
* @param {string} userId The user Id (usually a UUID as a string).
* @param {Number} durationSeconds The duration of all JWTs in seconds.
*/
public static revoke(userId: string, durationSeconds: number): void {
JWTManager.revokedJWTs[userId] = Date.now() + (durationSeconds * 1000);
}
/**
* Cleans up the cache to remove old user's that have expired.
* @private
*/
static _cleanUp(): void {
const now = Date.now();
Object.keys(JWTManager.revokedJWTs).forEach((item, index, _array) => {
const expiration = JWTManager.revokedJWTs[item];
if (expiration < now) {
delete JWTManager.revokedJWTs[item];
}
});
}
}
/**
* Set an interval to clean-up the cache, call .unref() to allow the process to exit without manually calling clearInterval.
*/
setInterval(JWTManager._cleanUp, 7000).unref();
Our backend also needs to ensure that it checks JWTs with the JWTManager
on each API call. This has far less impact to the client than calling out to the IdP, as it is in-memory.
router.get('/todo', function(req, res, next) {
const jwt = _parseJWT(req);
if (!JWTManager.isValid(jwt)) {
res.sendStatus(401);
return;
}
// ...
});
A few final configuration steps. Again, these are specific to FusionAuth and may be different for your auth management system. First, we need to make sure that JWT revocation webhooks are enabled in the tenant.
And then we configure our Webhook in FusionAuth, making sure to the jwt.refresh-token.update
event is enabled:
We can now revoke a user’s refresh token and FusionAuth will broadcast the event to our Webhook. The Webhook then updates the JWTManager which will cause JWTs for that user to be revoked.
This solution works well even in large systems with numerous backends. It requires the use of refresh tokens and an API that allows refresh tokens to be revoked. The only caveat is to be sure that your JWTManager code cleans up after itself to avoid running out of memory. Also, if you are using a distributed system with multiple servers running the Todo Backend, you could store the list of revoked JWTs in redis or another in memory datastore and have the JWTManager
update that when a webhook arrives.
If you are using FusionAuth, you can use the Webhook and Event system to build this feature into your application quickly. We are also writing JWTManager
implementations into each of our client libraries so you don’t have to write those yourself. At the time of this writing, the Java and Typescript clients both have a JWTManager
you can use. The other languages might have a JWTManager implementation now but if they don’t, just submit a support ticket or a GitHub issue and we will write one for you.