Authentication with JWT in Rails API
Authentication is one of the vital parts of any web application, and there are many libraries that provide various options to perform authentication in one way or another. The most prominent gem for authentication in Ruby on Rails is devise
which is considered to be an overkill for API-based systems.
For the fact that JWT(JSON web token) gives me total control over my whole authentication process, I prefer using it when it comes to building APIs. In this tutorial, we are going to be talking about how JWT can be used for authenticating Rails API, and at the same time touching a bit of authorization to make it more explanatory and interesting.
Note: This tutorial focuses only on authentication in Rails
and not how API is built in Rails.
If you are looking for an extensive tutorial on Rails API, kindly check this out https://scotch.io/tutorials/build-a-restful-json-api-with-rails-5-part-one. Now let’s get started.
- Add
jwt
anddotenv-rails
to your Gemfile:
gem 'jwt'
gem 'dotenv-rails'
jwt
is the main gem that handles tokens encoding and decoding. dotenv-rails
will help us keep our jwt secret key as an environmental variable.
Then run bundle install
2. Create a .env
file in the root directory and add your jwt secret key:
JWT_SECRET='yoursecretkey'
3. We will create our JsonWebToken
class which will contain the logic to encode and decode tokens. So create a jwt
folder inside your app
folder and add a json_web_token.rb
file.
app/jwt/json_web_token.rb:
class JsonWebToken
JWT_SECRET = ENV["JWT_SECRET"] def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i JWT.encode(payload, JWT_SECRET)
end def self.decode(token)
body = JWT.decode(token, JWT_SECRET)[0]
HashWithIndifferentAccess.new body
rescue JWT::ExpiredSignature, JWT::VerificationError => e
raise ExceptionHandler::ExpiredSignature, e.message
rescue JWT::DecodeError, JWT::VerificationError => e
raise ExceptionHandler::DecodeError, e.message
end
end
We are declaring the constant JWT_SECRET
in the first line of the class which had already been defined in the .env
file. This class contains essentially two class methods, encode
and decode.
The encode
method accepts two parameters, a payload which is a hash containing key-value you are encoding with, and the token expiry time which we’ve set to a default value of 24hrs. This method relies on the encode
method from the JWT
class.
The decode
method accepts token as the only parameter, and also relies on the provided decode
method from JWT
class. HashWithIndifferentAccess
gives permission to refer to the keys in the decoded body
as both symbol and string. We are also raising ExpiredSignature
and DecodeError
exceptions which are going to be rescued soon to display appropriate custom messages.
4. Let’s create an exception class where we will rescue and provide custom messages for the JWT exceptions we’ve raised.
app/controllers/concerns/exception_handler.rb:
module ExceptionHandler
extend ActiveSupport::Concern class DecodeError < StandardError; end
class ExpiredSignature < StandardError; end included do
rescue_from ExceptionHandler::DecodeError do |_error|
render json: {
message: "Access denied!. Invalid token supplied."
}, status: :unauthorized
end rescue_from ExceptionHandler::ExpiredSignature do |_error|
render json: {
message: "Access denied!. Token has expired."
}, status: :unauthorized
end
end
end
The included
method which takes a block of code makes our rescue logic available as soon as this module is included into any class. We then created the exception classes, inheriting from StandardError.
Each of the exception classes are rescued by responding with appropriate custom messages.
5. Let’s include
the ExceptionClass
to the application_controller
so that it can be made visible to all our controllers.
app/controllers/application_controller.rb:
class ApplicationController < ActionController::API
include ExceptionHandlerend
6. Now it’s time to create our authentication
and authorization
files.
app/auth/authentication.rb:
class Authentication
def initialize(user_object)
@username = user_object[:username]
@password = user_object[:password]
@user = User.find_by(username: @username)
end def authenticate
@user && @user.authenticate(@password)
end def generate_token
JsonWebToken.encode(user_id: @user.id)
end
end
Our Authentication
constructor accepts user_object
as a parameter, this is a hash containing both the username
and password
of the user. The authenticate
method simply checks if the user is valid by comparing the username
with the hashed password
. The generate_token
calls the encode
method of our JsonWebToken
class, and supplies a hash of the user’s id.
app/auth/authorization.rb:
class Authorization
def initialize(request)
@token = request.headers[:HTTP_TOKEN]
end def current_user
JsonWebToken.decode(@token)[:user_id] if @token
end
end
Our Authorization
constructor accepts the request
object as a parameter, and from there, extracts the supplied token. The current_user
method decodes and gets user_id
from the decoded token.
7. The Authentication
and Authorization
classes are now available for use in any of our controllers. Firstly, let’s see how we can use the Authentication
class to control login action inside the users_controller
.
app/controllers/users_controller.rb:
class UsersController < ApplicationController def login
auth_object = Authentication.new(login_params)
if auth_object.authenticate
render json: {
message: "Login successful!", token: auth_object.generate_token }, status: :ok
else
render json: {
message: "Incorrect username/password combination"}, status: :unauthorized
end
end private def login_params
params.permit(:username, :password)
end
end
The private method login_params
returns a hash containing both the username
and password
from the request body. This a very common way of making sure that the data coming from the request body is secured.
The Authentication
class is then instantiated, passing the login_params
. The authenticate
method is called to ensure validity of the user after which an appropriate message is sent. If the user is valid, the generate_token
method is called so as to include token
in the response message.
To demonstrate an authorization example, let’s assume there’s a Group
model in this application and there is a feature for posting messages to groups whereby only the creator of a group is permitted to post messages.
app/controllers/groups_controller.rb:
class GroupsController < ApplicationController def post_message
authorization_object = Authorization.new(request)
current_user = authorization_object.current_user if current_user == Group.find(params[:id]).created_by
# post message
else
# respond: You are not allowed to post to this group
end
end
end
I believe this covers the basics of how JWT can be integrated in Rails. In the second part of this tutorial, I will be explaining how JWT based requests can be tested with rspec.
If you have any suggestion/comment /question as regards this tutorial, kindly post as comment below.