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.

Further Reading