This is just some code I recently used in my development application in order to add token-based authentication for my api-only rails app. The api-client was to be consumed by a mobile application, so I needed an authentication solution that would keep the user logged in indefinetly and the only way to do this was either using refresh tokens or sliding sessions.
I also needed a way to both blacklist and whitelist tokens based on a unique identifier (jti)
Before trying it out DIY, I considered using:
- devise-jwt which unfortunately does not support refresh tokens
- devise_token_auth I ran into issues when it came to the changing headers on request on mobile, disabling this meant users would have to sign in periodically
- doorkeeper This was pretty close to what I needed, however, it was quite complicated and I considered it wasn't worth the extra effort of implmeneting OAuth2 (for now)
- api_guard This was great, almost everything I needed but it didn't play too nicely with GraphQL and I needed to implement token whitelisting also.
So, since I couldn't find any widely-used gem to meet my needs; I decided to just go DIY, and the end result works pretty well. And overview of how things works is so:
- You call on the
Jwt::Issuer
module to create anaccess_token
andrefresh_token
pair. - You call on the
Jwt::Authenticator
module to authenticate theaccess_token
get thecurrent_user
and thedecoeded_token
- You call on the
Jwt::Revoker
module to revoke (blacklist/remove whitelist) a token - You call on the
Jwt::Refresher
module to refresh anaccess_token
based on a refresh_token
There are more modules, but you can preview them for yourself.
There are some prequistes you need in order to use this code:
-
You need to create a blacklisted tokens table like so:
rails g model BlacklistedToken jti:string:uniq:index user:belongs_to exp:datetime
-
If you want to use whitelisting to, create a tokens table like so:
rails g model WhitelistedToken jti:string:uniq:index user:belongs_to exp:datetime
-
Create a refresh tokens table like & model so:
rails g model RefreshToken crypted_token:string:uniq user:belongs_to
class RefreshToken < ApplicationRecord
belongs_to :user
before_create :set_crypted_token
attr_accessor :token
def self.find_by_token(token)
crypted_token = Digest::SHA256.hexdigest token
RefreshToken.find_by(crypted_token: crypted_token)
end
private
def set_crypted_token
self.token = SecureRandom.hex
self.crypted_token = Digest::SHA256.hexdigest(token)
end
end
- Update the user model to include the associations
has_many :refresh_tokens, dependent: :delete_all
has_many :whitelisted_tokens, dependent: :delete_all
has_many :blacklisted_tokens, dependent: :delete_all
Then you are pretty much ready to go!
In the future, I might make this into a gem or add redis support or similar.
I hope this gist helps someone!
Hey there @jesster2k10, great work! It looks like a really readable, testable piece of code :D
I have one question: do you blacklist access_token intentionally? From my perspective, it seems incorrect to store JWT access_token in the database and on each authentication query on DB (checking if the token is blacklisted or not). It seems to me like we are losing the biggest advantage of JWT itself. Could you elaborate a bit on the idea of blacklisting refresh_token (ofc with keeping TTL of access token relatively short)? It would prevent future refreshing tokens, ofc downside is that previous access_token can be still used for couple minutes.