.   .   .
We're always looking for great talent‼️ 🚀😄 And we're passionate about driving innovation and improving lives. Join us as we help social impact organizations and enterprise customers build their web apps. Apply today!
.   .   .

This blog post is heavily inspired by this. It didn’t quite work for me so I made some modifications and figured I’d write a post about it. This tutorial assumes that you already have a standard Devise set up on a rails 4 environment.

Install the ruby-jwt gem

Add the following to your gem file:

gem ‘jwt’, ‘~> 1.5.1’

Then install the gem with bundle install

Create a file called app/controllers/api/base_controller.rb with the following content:

# app/controllers/api/base_controller.rb
class Api::BaseController < ActionController::Base
  include ActionController::ImplicitRender
  respond_to :json
 
  before_filter :authenticate_user_from_token!
 
  protected
 
  def current_user
    if token_from_request.blank?
      nil
    else
      authenticate_user_from_token!
    end  
  end
  alias_method :devise_current_user, :current_user
 
  def user_signed_in?
    !current_user.nil?
  end
  alias_method :devise_user_signed_in?, :user_signed_in?
 
  def authenticate_user_from_token!
    if claims and user = User.find_by(email: claims[0]['user'])
      @current_user = user
    else
      return render_unauthorized
    end
  end
 
  def claims
    JWT.decode(token_from_request, "YOURSECRETKEY", true)
  rescue
    nil
  end
 
  def jwt_token user
    # 2 Weeks
    expires = Time.now.to_i + (3600 * 24 * 14)
    JWT.encode({:user => user.email, :exp => expires}, "YOURSECRETKEY", 'HS256')
  end
 
  def render_unauthorized(payload = { errors: { unauthorized: ["You are not authorized perform this action."] } })
    render json: payload.merge(response: { code: 401 }), status: 401
  end
 
  def token_from_request
    # Accepts the token either from the header or a query var
    # Header authorization must be in the following format
    # Authorization: Bearer {yourtokenhere}
    auth_header = request.headers['Authorization'] and token = auth_header.split(' ').last
    if(token.to_s.empty?)
      token = request.parameters["token"]
    end
 
    token
  end
 
end

Update the first line of your app/controllers/application_controller.rb to this:

#app/controllers/application_controller.rb
class ApplicationController < Api::BaseController

Create a file called app/controllers/api/sessions_controller.rb with the following content:

# app/controllers/api/sessions_controller.rb
class Api::SessionsController < Api::BaseController
  skip_before_filter :authenticate_user_from_token!
  before_filter :ensure_params_exist
 
  def create
    @user = User.find_for_database_authentication(email: user_params[:email])
    return invalid_login_attempt unless @user
    return invalid_login_attempt unless @user.valid_password?(user_params[:password])
    @auth_token = jwt_token({ email: @user.email})
  end
 
  private
 
  def user_params
    params.require(:user).permit(:email, :password)
  end
 
  def ensure_params_exist
    if user_params[:email].blank? || user_params[:password].blank?
      return render_unauthorized errors: { unauthenticated: ["Incomplete credentials"] }
    end
  end
 
  def invalid_login_attempt
    render_unauthorized errors: { unauthenticated: ["Invalid credentials"] }
  end
end

Create a file called app/controllers/api/registrations_controller.rb with the following content

# app/controllers/api/registrations_controller.rb
class Api::RegistrationsController < Api::BaseController
  skip_before_filter :authenticate_user_from_token!
  def create
    @user = User.new(user_params)
    @auth_token = jwt_token({ email: @user.email}) if @user.save
  end
 
  private
 
  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :provider, :uid, :name)
  end
 
end

Create a file called app/views/api/registrations/create.json.jbuilder with the following content:

# app/views/api/registrations/create.json.jbuilder
if @user.errors.present?
  json.errors @user.errors.messages
  json.response do
    json.code 422
  end
else
  json.data do
    json.token @auth_token
  end
  json.response do
    json.code 201
  end
end

Create a file called app/views/api/sessions/create.json.jbuilder with the following content

# app/views/api/sessions/create.json.jbuilder
json.token @auth_token

Create a file called lib/jsonwebtoken.rb with the following content:

#require 'jwt'
class JsonWebToken
  def self.encode(payload, expiration = 24.hours.from_now)
    payload = payload.dup
    payload['exp'] = expiration.to_i
    JWT.encode(payload, Rails.application.secrets.json_web_token_secret)
  end
 
  def self.decode(token)
    JWT.decode(token, Rails.application.secrets.json_web_token_secret)
  end
end

Update your routes

In your config/routes.rb, replace the following

devise_for :users

with the following. We need to override the sessions and registrations controllers to work with our JWT Tokens

devise_for :users, :controllers => {sessions: ‘api/sessions’, registrations: ‘api/registrations’}

Bypassing authentication

With this setup, every controller will require a token except for the registration and sessions controller. If you want to allow another controller to bypass the token authorization, you can use the following:

skipbeforefilter :authenticateuserfrom_token!

Usage with a client

Now when you login to the site using the login endpoint, you’ll get a token. Save that token on your client. Now with every request you make to the rails api you can use that token in the authorization header using the following format.

Authorization: Bearer {yourtokenhere}

Alternatively, you can just add ?token={yourtokenhere} to the end of every URL. But that may or may not be a good decision depending on the security profile of your application.

Closing thoughts

If you have any questions or notice anything wrong, just let me know and I’ll get it taken care of! If you need help integrating JWT in your application, feel free to reach out to my team at labs@chiedo.com.

.   .   .

We're Hiring‼️ ?? Looking to join our team of web developers? We're passionate about innovation, family, and community. Apply today!