Stateful JSON Web Tokens

Trevor Hinesley
Soundstripe Engineering
4 min readAug 3, 2018

“Wait… the whole point of JSON Web Tokens is to be stateless…”

Before you disregard the idea, let’s look at the biggest reason you might want a stateful token: revocation.

This article details how to implement session-backed JWTs into your application, and the benefits of doing so.

The Problem

There are many benefits to using JWTs as an encrypted token protocol, particularly the relative ease of encryption/decryption, the ability to use different encryption algorithms, and the self-contained nature of the tokens. Using JWTs as a bearer token is also a simpler way to manage authentication when OAuth may be a bit overkill (though you can obviously use them together; they’re complimentary, not mutually exclusive).

At Soundstripe, when we were first setting up an authentication mechanism and vetting different protocols and options, there were a few things we knew we needed it to be:

  1. Simple. There are many use cases which require extremely complex and necessarily thorough authentication schemes: ours was not one of those. We didn’t have numerous internal applications, so we didn’t need SSO.
  2. Dynamic. The mechanism should allow some sort of flexibility. We knew we’d have different account types, as well as different uses for tokens than just authentication, so it would be ideal if we could use one codec which worked for a range of applications.
  3. Trusted. We weren’t trying to reinvent the wheel; we just needed a secure and reliable way to authenticate. We were comfortable leaving the low-level details up to experts in security.
  4. Controllable. We needed something which would allow us to manage logging users out if needed and to revoke compromised tokens.

We loved the idea of using JWTs, but they weren’t revokable. We could have used short token expirations, but then you have to implement token refreshing, which is part of the overhead that comes with using OAuth, something we wanted to avoid.

The Solution

In the JWT spec (on top of being able to use whatever private claims you want), it mentions an interesting optional claim: jti. Here’s what it says:

The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" claim can be used to prevent the JWT from being replayed. The "jti" value is a case-sensitive string. Use of this claim is OPTIONAL.

Notice the piece at the bottom…

The “jti” claim can be used to prevent the JWT from being replayed.

So, we took can to be literal :)

Instead of using the jti claim to prevent replay, maybe we could give each token a unique ID as its jti claim, persist the ID in a session in the database, and ensure a persisted session exists in the database with the corresponding ID.

Let’s look at a simplified, example implementation in Ruby.

First, the Session model with a column in which to store the jti:

models/session.rb

It’s pretty bare bones, but it gets the job done. Next, we need to be able to log users in by creating a JWT and backing it with a session. Let’s use the jwt gem:

controllers/sessions_controller.rb

Lastly, we need to verify a token is valid if it’s passed as a header:

controllers/application_controller.rb

Now we have tokens which last 30 days, include the user’s ID as the sub claim, and are revokable!

Sure, this adds at least one extra database query on each authenticated request, but with proper indexing, it’s pretty negligible (and in our experience, it has been well worth the tradeoff). It also offers two awesome benefits:

  1. Another layer of token validation! Even if someone manages to recreate the key you’re using to sign your tokens (unlikely), they still couldn’t create a valid token with it. Now, there has to be a session in the database corresponding to the token, proving our own API generated it and not an outside entity. If a malicious party is able to bypass even this piece, and somehow insert a session into your database to validate the token, you’ve got bigger problems to worry about.
  2. Control. Bingo. We are now able to immediately invalidate tokens by simply removing a session from the database (or setting a flag on the session, whichever suits your use case). Also, if for some reason we need to log out a user(s), we can now do so fairly easily. With OAuth or a stateless JWT implementation, you have to wait for the token to expire or rotate your signing key, potentially causing other headaches.

This implementation allows us to use long-lived tokens, without fear of CSRF (the token is passed as a header, after all), and without worrying about tokens with long expiration being leaked. Obviously, like with any authentication mechanism, if someone gets your authentication credentials (your token in this case), they can act as you. We don’t want anyone accessing our users’ tokens under any circumstance, but with our implementation, if someone does manage to get it from a user through nefarious means, we have a simple way to retain full control over the token.

Also, you can use the user’s ID (available as the sub claim in the payload) to instantiate the current_user if you need to!

I hope this was helpful to anyone trying to accomplish the same things we were. I’m sure there are myriad other ways to accomplish the same goals, but this has worked well for us. If you have any questions or comments, feel free to leave them here!

P.S. If you need awesome music for marketing videos, a podcast, etc. — check out our library at soundstripe.com!

--

--