An Introduction to Using JWT Authentication in Rails
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.