Ruby
Article

An Introduction to Using JWT Authentication in Rails

By Devdatta Kane

With the advent of Single Page Applications (SPA) and mobile applications, APIs have come to the forefront of web development. As we develop APIs to support our SPA and mobile apps, securing the APIs has been a major pain area. Token-based authentication is one of the most-favored authentication mechanisms, but tokens are prone to various attacks. To mitigate that, one has to implement ways to fix the issues, which often leads to one-off solutions that make tokens non-exchangeable between diverse systems. JSON Web Tokens (JWT) were created to implement standards-based token handling and verification that can be exchanged between diverse systems without any issue.

What is JWT?

JWTs carry information (called “claims”) via JSON, hence the name JSON Web Tokens. JWT is a standard and has been implemented in almost all popular programming languages. Hence, they can be easily used or exchanged in systems implemented in diverse platforms.

JWTs are comprised of plain strings, so they can be easily exchanged in a URL or a HTTP header. They are also self-contained and carry information such as payload and signatures.

Anatomy of a JWT

A JWT (pronounced ‘JOT’) consists of three strings separated by ‘.’:

aaaaa.bbbbbbb.ccccccc

The first part is the header, second part is the payload, and third part is the signature.

The header consists of two parts:

  • The type of token, i.e. ‘JWT’
  • The hashing algorithm used

For example:

{
  "typ": "JWT",
  "alg": "HS256"
}

The header is base64 encoded which results in:

aeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

This is the first part of the token.

The second part of a JWT is the payload. This part carries the interesting information in the token, also called as JWT Claims. Claims are of three types – private, public, and registered.

  • Registered Claims are claims whose names are reserved but are not mandatory to be used. Examples being – iss, sub, aud, etc.
  • Private Claims are names that are agreed upon between two parties and can collide with other public claims. Must be used with caution.
  • Public Claims – Claims that we can create as per our authentication requirements, such as username, user information, etc.

We can create a sample payload like so:

{
  "iss": "sitepoint.com",
  "name": "Devdatta Kane",
  "admin": true
}

This will be encoded as –

ew0KICAiaXNzIjogInNpdGVwb2ludC5jb20iLA0KICAibmFtZSI6ICJEZXZkYXR0YSBLYW5lIiwNCiAgImFkbWluIjogdHJ1ZQ0KfQ

This becomes the second part of token.

The third and, possibly, the most important part is the Signature. It’s a hash of three components: the header, the payload, and the secret. We run the combined string of the header and the payload through an HMACSHA256 function with ‘secret’ as the server-side secret. Like so:

require "openssl"
require "base64"

var encodedString = Base64.encode64(header) + "." + Base64.encode64(payload);
hash  = OpenSSL::HMAC.digest("sha256", "secret", encodedString)

Since only the server knows the secret, no one can tamper with the payload and the server can detect any tampering using the signature.

Our signature looks as follows:

2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37

Now we have got all three parts of our JWT. Combining the three parts, we get:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ew0KICAiaXNzIjogInNpdGVwb2ludC5jb20iLA0KICAibmFtZSI6ICJEZXZkYXR0YSBLYW5lIiwNCiAgImFkbWluIjogdHJ1ZQ0KfQ.2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37

This is our complete JWT which can be used for further requests.

Using JWT in Rails

JWT has libraries for almost all platforms and Ruby is no exception. We will create a simple Rails application which uses the excellent Devise gem for authentication and the jwt gem for creating and verifying JWT tokens.

Let’s create a sample Rails application with a contact model and CRUD. The app uses Rails 4.2 and SQlite:

rails new jwt_on_rails

After the application is generated, create a Home controller which we will use to check our authentication. Here is our home_controller.rb in the app/controllers. Like so:

class HomeController < ApplicationController
  def index

  end
end

Map the HomeController to /home in config/routes.rb:

Rails.application.routes.draw do
  get 'home' => 'home#index'
end

Check how its working:

rails s

Point your favorite browser to http://localhost:3000/ and check if everything is working properly.

We have our base application ready. Now, add Devise to our application. First, we will add the Devise and jwt gems in our Gemfile. Like so:

gem 'devise'
gem 'jwt'

Install them using:

bundle install

Now let’s create the Devise configuration files:

rails g devise:install

We will create the Devise User model and migrate the database:

rails g devise User
rake db:migrate

Our User model is in place, which we will use for authentication. It’s time to integrate jwt into our application. First, we will create a class named JsonWebToken in lib/json_web_token.rb. This class will encapsulate the JWT token encoding and decoding logic. Like so:

class JsonWebToken
  def self.encode(payload)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

  def self.decode(token)
    return HashWithIndifferentAccess.new(JWT.decode(token, Rails.application.secrets.secret_key_base)[0])
  rescue
    nil
  end
end

Add an initializer for including the JsonWebToken class in config/initializers/jwt.rb. Like so:

require 'json_web_token'

We will now add some helper methods in the ApplicationController class which we will use in AuthenticationController class.

In app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  attr_reader :current_user

  protected
  def authenticate_request!
    unless user_id_in_token?
      render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      return
    end
    @current_user = User.find(auth_token[:user_id])
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end

  private
  def http_token
      @http_token ||= if request.headers['Authorization'].present?
        request.headers['Authorization'].split(' ').last
      end
  end

  def auth_token
    @auth_token ||= JsonWebToken.decode(http_token)
  end

  def user_id_in_token?
    http_token && auth_token && auth_token[:user_id].to_i
  end
end

What we’ve done here is added a few helper methods like authenticate_request! which will act as a before_filter to check user credentials. We will create an AuthenticationController to handle all authentication requests to the API. Like so:

In app/controllers/authentication_controller.rb:

class AuthenticationController < ApplicationController
  def authenticate_user
    user = User.find_for_database_authentication(email: params[:email])
    if user.valid_password?(params[:password])
      render json: payload(user)
    else
      render json: {errors: ['Invalid Username/Password']}, status: :unauthorized
    end
  end

  private

  def payload(user)
    return nil unless user and user.id
    {
      auth_token: JsonWebToken.encode({user_id: user.id}),
      user: {id: user.id, email: user.email}
    }
  end
end

Here we have added AuthenticationController to implement the authentication endpoint. It uses Devise to authenticate the user and issue a JWT if the credentials are valid.

We will now update our routes.rb to add the authentication endpoint. Like so:

Rails.application.routes.draw do
  post 'auth_user' => 'authentication#authenticate_user'
  get 'home' => 'home#index'
end

Also, modify the HomeController to secure it using a before_filter and add a meaningful response in case of successful authentication:

class HomeController < ApplicationController
  before_filter :authenticate_request!

  def index
    render json: {'logged_in' => true}
  end
end

Now, create a sample user to test the authentication mechanism using Rails Console.

rails c
rails> User.create(email:'a@a.com', password:'changeme', password_confirmation:'changeme')

Start the server and check out how JWT authentication works:

rails s

Open another terminal and use cURL to test the API. First, try to authenticate without any email or password:

curl http://localhost:3000/home

The response should be {"errors":["Not Authenticated"]} since we have not provided any credentials.

Now authenticate against the API and receive a JWT which we will use for subsequent requests:

curl -X POST -d email="a@a.com" -d password="changeme" http://localhost:3000/auth_user

You’ll receive a successful response along with a JSON Web Token and additional user information. Like so:

{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.po9twTrX99V7XgAk5mVskkiq8aa0lpYOue62ehubRY4","user":{"id":1,"email":"a@a.com"}}

Use our fresh auth_token in the request to /home. Like so:

curl --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.po9twTrX99V7XgAk5mVskkiq8aa0lpYOue62ehubRY4" http://localhost:3000/home

We should receive a successful login response, like:

{"logged_in":true}

Conclusion

We can now utilize this API in any Angular/React/Ember application by storing the issued JWT (in a cookie or local storage) and using it in subsequent requests. This wraps up our tutorial in which we learned how to implement JWT in a Rails application along with Devise. While this tutorial covered just the basics, it is foundational to using JWTs for API authentication.

Hope you liked this tutorial. Comments and feedback welcome, as always.

  • http://twitter.com/kenn Kenn Ejima

    It sounds like JWT is not suitable for some use cases where replay attack needs to be prevented. You can’t tamper the header or payload, but you can capture and re-post the same valid content again and again, which could be a problem for non-idempotent APIs like posting an additive score (or item gain) in a game, or check-ins.

  • Neeraj Murarka

    I was getting the following error that I do not think is related to JWT:

    ActionController::InvalidAuthenticityToken

    I set the following value for protect_from forgery in my application_controller.rb:

    protect_from_forgery with: :null_session

    Problem resolved!

  • George Opritescu

    The part: auth_token[:user_id].to_i doesn’t seem to be the best choice here. nil.to_i or ‘a’.to_i both return 0.

  • http://twitter.com/kenn Kenn Ejima

    I wouldn’t recommend that approach – it’s susceptible to race conditions with parallel API calls and/or retries, particularly on slow mobile devices. Plus, UPDATE is a costly (and one of the most costly on Postgres compared to INSERTs or DELETEs) write operation at scale.

    One way to (possibly dramatically) mitigate the risk is to include timestamp (should also be included in signature), so requests with too old timestamp can be ignored. But still there’s a small window of replay attack so as you said there’s no perfect solution I guess…

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.