Token Based Authentication with JWT in Rails
This past summer, I worked on a project building out an API for a mobile application. I thought it would be cool to use token based authentication in order to secure the API. To accomplish this authentication scheme I used JSON Web Tokens and Redis.
Gemfile
For this project I used the jwt gem and the redis gem. The JWT gem provides a nice abstraction for encoding and decoding JWTs.
gem 'jwt'
gem 'redis'
gem 'bcrypt'
Models
I've also used the bcrypt gem in order to make the actual authentication of the user super easy. With this we just need a User model where the table has an email and password_digest columns. Then in the model itself we just need to call has_secure_password.
class User < ActiveRecord::Base
has_secure_password
# ... everything else.
end
Then I created a service object to wrap the authentication call to user, as well as guard against invalid data.
class Session
def self.authenticate(email, password)
return false if email.blank? || password.blank?
user = User.find_by(email: email)
user && user.authenticate(password) ? user : false
end
end
Application Controller
class ApplicationController < ActionController::Base
respond_to :json
before_action :authenticate
protected
def current_token
@token || nil
end
def current_user
@current_user ||= User.find($redis.hget(current_token, :user_id)) if current_token
end
def authenticate
authenticate_token || render_unauthorized
end
def authenticate_token
authenticate_with_http_token do |token, options|
@token = nil
if AuthToken.valid?(token) && $redis.ttl(token) > 0
@token = token
$redis.expire(token, 20.minutes.to_i) # set TTL as constant
end
@token
end
end
def render_unauthorized
self.headers['WWW-Authenticate'] = 'Token realm="Application"'
render nothing: true, status: :unauthorized, content_type: 'application/json'
end
end
Session Controller
class SessionsController < ApplicationController
skip_before_action :authenticate, only: :create
def create
if user = Session.authenticate(params[:email], params[:password])
token = AuthToken.issue(user_id: user.id)
$redis.hset(token, 'user_id', user.id)
$redis.expire(token, 20.minutes.to_i)
render json: {user: user, token: token}
else
render json: { error: 'Invalid email or password' }, status: :unauthorized
end
end
def destroy
$redis.del(current_token)
render nothing: true, status: :ok, content_type: 'application/json'
end
end
The final piece is a helper for to issue and validate the tokens using the JWT library. I just made a wrapper method around the JWT calls to simplify things a bit more.
module AuthToken
def AuthToken.issue(payload)
payload[:created_at] = Time.now.utc.to_i
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def AuthToken.valid?(token)
JWT.decode(token, Rails.application.secrets.secret_key_base) rescue false
end
end
How It Works
When a request is made from the client application to the API, the generated token goes in the Authorization header of the HTTP request as such:
Authorization: Token token=<the-generated-token>
The request will hit the authenticate method in ApplicationController. This then calls the authenticate_token method, which gets the token from the HTTP header using authenticate_with_http_token. Then a check is made to see if the token is valid by seeing if it can be decoded via the JWT library. If the token is valid a request is made via redis to see if the token has expired or not. If it is valid and has not expired the user is then authenticated. The expiration time on the token stored in redis is reset to 20 minutes (this could be any TTL that you want). If any of these checks fail a call to render_unauthorized is made and sent back to the client.
While we're going over the ApplicationController, take a look at the current_user method. In order to find the appropriate user for a token, a request is made to redis to get the value of the 'user_id' key. This is what was stored there when the user logged in successfully, which is what we'll talk about next.
When a user wants to authenticate with the API, a request is made to sessions#create. The action then checks to see if the user's credentials are valid. If they are a new token is generated for the user. The user id is then stored in Redis nested under the token string. Finally, an expiration time is set for 20 minutes on the token key in Redis.
To log the user out, the client will make a request to sessions#destroy and discard the token. The destroy action removes the key from Redis.
I found this approach to be really straightforward. There isn't too much code involved to get this to work! Some advantages of using JWT for token based authentication is the fact it can store data. If you noticed in the code examples above, the user_id is set as well as when the token was created as the payload of the JWT token. This can potentially be used to pass data around to other services that you use in your application. As long as they have the secret key those services will be able to decode the token and get access to that data. In the same vein, using Redis as a session store could also allow other services that make up your application access to this type of information, as well as provide their own checks to see if the user is logged in or not. Redis also provides the mechanism to expire a key, which is great not to have to worry about. JWT also provides a way set an expiration on the JWT token itself. As of writing this, you can add a reserved key, "exp", to the JWT payload with the time that the token is no longer valid. Now I wouldn't use this expiration by itself, because you would need to issue a new token for every request if you wanted to have a reasonable expiration to the session, which seems kind of annoying to do. But I could see it being used in conjunction with the Redis based expiration to ensure the user re-authenticates at some interval (presumably some large interval).
There are some potential drawbacks to using this approach. If a user is logged in and a token is generated, the same token can be copied and potentially used in multiple clients. This could be a problem if you don't use SSL on your application, since it would be open to a man in the middle attack where the token could be taken from (but you sould be using SSL on your application ;)). Similarily, the token could be copied from the client itself and used on aother client. While this wasn't necessarily an issue for the project I was working on since it was a native mobile application (and it would be harder for someone to get access to your phone and to read the value out of memory), this could be an issue on javascript client applications and is something to be aware of. Although, I think the risk is small.
I also want to point out, that using Redis as a session store isn't completely necessary in order for this to work. You could have your application set up in a way where the token is valid for as long as the client application has it and it's valid.
Expansion
This strategy seemed to work really well for me and I'll definitely be using it again for future projects (unless someone finds a hole in what's been done). One thing I would like to do is clean up the code slightly (the code in this article is pretty much as it was in the application). While it isn't too bad I definitely think it can be tightened up a bit, especially around the parts with redis.