Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brianjbayer/26c580c6267f3f951924da923dfb2d3b to your computer and use it in GitHub Desktop.
Save brianjbayer/26c580c6267f3f951924da923dfb2d3b to your computer and use it in GitHub Desktop.
How to use an integer-based counter as a simple and performant mechanism for revoking a user's JSON Web Token (JWT)

Using a Counter for Simple and Performant JWT Revocation

Pioneertown Jail, CA - Wendy Bayer

📷 Photo: Pioneertown Jail, CA by Wendy Bayer


One of the biggest challenges with JSON Web Tokens (JWTs) as an authentication and/or authorization mechanism is the ability to easily and quickly revoke the token if it becomes compromised.

Often Allowlists and/or Denylists are used for ensuring the validity of JWTs, but these required database lookups defeat the purpose of using a JWT over using sessions.

This post presents a rather simple and performant method of revoking a JWT using a counter for each user.


Origin Story

I learned this technique from an Application Security training session we received at CoverMyMeds from Jim Manico and Dr. Justin Collins

In reality it was more of an aside relegated to a single slide that said...

Side note on revocation

Why not associate a counter value with each user
Embed the counter into the JWT, and keep a copy in the database
More lightweight than keeping track of issued identifiers

Revoking JWTs for a user account is as simple as incrementing the counter

Validating a JWT requires a check against the stored counter value
A match means that the JWT is not revoked
A stored counter value that is higher than the JWT value means revocation


An Implementation

Here is an implementation of this idea that I developed in a personal project where I used a JWT for authorization.

Add the JWT Revocation Counter to the User Model

To add a revocation counter to your application or service's User model, you will...

  1. Add a database migration to add the counter to the user database table
  2. Update the User model to add validations for the counter as well as methods to check if the JWT has been revoked and to revoke the JWT using the counter
  3. Add tests for the added validations and methods

The Migration

You will need to add the new JWT counter to your user database table. This example is for a PostgreSQL database with an added JWT counter named authorization_min. A bigint is being used to defer/mitigate the issue of having to reset this counter to avoid a "rollover". You would not want a revoked JWT to appear valid because its counter value rolled over.

Ideally, you would set up a separate monitoring job to detect when the counter is approaching rollover and to reset it.

Finally, since the approach is to consider a JWT as revoked if the stored counter value is greater than the value in the JWT, we set the initial (default) value of this stored counter to the minimum value for that data type.

class AddAuthorizationMinToUsers < ActiveRecord::Migration[7.0]
  def change
    # This assume PostgreSQL where -9223372036854775808 is lowest bigint value
    add_column :users, :authorization_min, :bigint, default: -9223372036854775808
  end
end

User Model Updates

You will need to update your User model for the added JWT counter. Here validations for presence and integer numericality are added. Methods are also added to abstract the counter operations of checking if the JWT is revoked and revoking the JWT.

Again, a JWT is considered revoked if the stored counter value is greater than the value in the JWT. Thus, to revoke a user's JWT, simply increment the stored counter in the database.

  validates :authorization_min, presence: true, numericality: { only_integer: true }

  def auth_revoked?(auth_value)
    authorization_min > auth_value
  end

  def revoke_auth
    update!(authorization_min: self.authorization_min += 1)
  end

User Model Test Updates

Here model tests are added for the new counter validations and methods. The validation tests use the Thoughtbot shoulda-matchers gem.

    describe 'authorization_min' do
      it { is_expected.to validate_presence_of(:authorization_min) }
      it { is_expected.to validate_numericality_of(:authorization_min).only_integer }
    end
  end

  describe 'methods' do
    subject(:user) { create(:user) }

    describe 'auth_revoked?' do
      it 'returns true when authorization_min > value' do
        user.authorization_min = Faker::Number.number
        expect(user.auth_revoked?(user.authorization_min - 1)).to be(true)
      end

      it 'returns false when authorization_min = value' do
        user.authorization_min = Faker::Number.number
        expect(user.auth_revoked?(user.authorization_min)).to be(false)
      end

      it 'returns false when authorization_min < value' do
        user.authorization_min = Faker::Number.number
        expect(user.auth_revoked?(user.authorization_min + 1)).to be(false)
      end
    end

    describe 'revoke_auth' do
      it 'increments authorization_min by 1' do
        initial_value = user.authorization_min
        user.revoke_auth
        expect(user.reload.authorization_min).to eql(initial_value + 1)
      end
    end

Add the Counter Value to the JWT When Issuing

You will need to add the current value of the user's stored JWT counter to the JWT when you issue it to that user which is specific to your JWT implementation. Here in this post, this value is named auth.

Add Logic to Validate the User's JWT with the Counter

Now that you have the JWT counter added to the User model and the user's JWT, you must add the logic to check this counter. If you are using the JWT for authentication, you will want to check this counter as part of your login processing. If you are using the JWT for authorization, you will want to check the counter for each operation requiring authorization.

Here is an example where a JWT is used for authorization and calls the auth_revoked? method added to the User model...

    def authorize_request
      decoded_jwt = decode_authentication_jwt(request_authorization_token)
      @current_user = User.find(decoded_jwt['user'])
      validate_token(@current_user, decoded_jwt)
    end

    ...

  def validate_token(user, token)
    raise Authorization::Errors::TokenRevokedError if user.auth_revoked?(token['auth'])
  end

Remaining Changes and Conclusion

Additional changes such as implementation of the JWT and revocation of the counter would be specific to your application/service and are not presented here. Hopefully however, this post does give you enough insight and information to be able to implement this approach to JWT revocation in your own project. If you would like to see a full example that uses this counter-based JWT revocation, see my personal project brianjbayer/random_thoughts_api


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment