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!
@jesster2k10
Thanks for this gist! I am using it for nextjs/rails app.
One area that stumped me was refreshing the access token using the
Refresher
module. Above, I kept running intoJWT::SignatureExpired
error whenever I tried decoding an expired access token. The only way to refresh the token was to setverify
to false while decoding it, otherwise the error wouldn't let me continue.Did you face this issue? If so, did you come up with a solution?