Author avatar

Hristo Georgiev

Token-based Authentication with Ruby on Rails 5 API

Hristo Georgiev

  • Dec 15, 2018
  • 17 Min read
  • 422,256 Views
  • Dec 15, 2018
  • 17 Min read
  • 422,256 Views
Ruby
Ruby on Rails

Introduction

With API-only applications so popular and Rails 5 right around the corner, the most common methods of authentication are now becoming token-based. In this guide, I'll give a short overview of token-based authentication and how it is implemented into a Rails 5 API-only application.

What Is Token-based Authentication?

Token-based authentication (also known as JSON Web Token authentication) is a new way of handling the authentication of users in applications. It is an alternative to session-based authentication.

The most notable difference between the session-based and token-based authentication is that session-based authentication relies heavily on the server. A record is created for each logged-in user.

Token-based authentication is stateless - it does not store anything on the server but creates a unique encoded token that gets checked every time a request is made.

Unlike session-based authentication, a token approach would not associate a user with login information but with a unique token that is used to carry client-host transactions. Many applications, including Facebook, Google, and GitHub, use the token-based approach.

Benefits of Token-based Authentication

There are several benefits to using such an approach:

Cross-domain / CORS

Cookies and CORS don't mix well across different domains. A token-based approach allows you to make AJAX calls to any server, on any domain, because you use an HTTP header to transmit the user information.

Stateless

Tokens are stateless. There is no need to keep a session store since the token is a self-contained entity that stores all the user information in it.

Decoupling

You are no longer tied to a particular authentication scheme. Tokens may be generated anywhere, so the API can be called from anywhere with a single authenticated command rather than multiple authenticated calls.

Mobile Ready

Cookies are a problem when it comes to storing user information in native mobile applications. Adopting a token-based approach simplifies this saving process significantly.

CSRF (Cross Site Request Forgery)

Because the application does not rely on cookies for authentication, it is invulnerable to cross-site request attacks.

Performance

In terms of server-side load, a network roundtrip (e.g. finding a session on a database) is likely to take more time than calculating an HMACSHA256 code to validate a token and parsing its contents. This makes token-based authentication faster than the traditional alternative.

How Does Token-based Authentication Work?

The way token-based authentication works is simple. The user enters his or her credentials and sends a request to the server. If the credentials are correct, the server creates a unique HMACSHA256 encoded token, also known as JSON web token (JWT). The client stores the JWT and makes all subsequent requests to the server with the token attached. The server authenticates the user by comparing the JWT sent with the request to the one it has stored in the database. Here is a simple diagram of the process:

token-based auth

What Does a JWT Token Contain?

The token is separated into three base-64 encoded, dot-separated values. Each value represents a different type of data:

Header

Consists of the type of the token (JWT) and the type of encryption algorithm (HS256) encoded in base-64.

Payload

The payload contains information about the user and his or her role. For example, the payload of the token can contain the e-mail and the password.

Signature

Signature is a unique key that identifies the service which creates the header. In this case, the signature of the token will be a base-64 encoded version of the Rails application's secret key (Rails.application.secrets.secret_key_base). Because each application has a unique base key, this secret key serves as the token signature.

Setting up a Token-based Authentication with Rails 5

Enough theory, it's time for practice. The first step is to create a new Rails 5 API-only application:

1rails _5.0.0.beta3_ new api-app --api
bash

By appending --api at the end of the generator, an API-only application will be created. API-only applications are recent additions to the Rails platform. An API application is a trimmed-down version of standard Rails application without any of the unnecessary middleware, such as .erb views, helpers, and assets. API applications come with special middlewares such as ActionController::API, request throttling, easy CORS configuration and other custom-waived features for building APIs.

There are several requirements that need to be met before we can use the token-based approach:

  • We need an accessible model.
  • A way of encoding and decoding JWT tokens must be implemented.
  • We need methods for checking if the user is authenticated.
  • Controllers for creating and logging in users are also necessary.
  • We need routes for creating users and logging them in and out.

Creating the User Model

First, the user model must be created:

1 rails g model User name email password_digest
bash

Run the migrations:

1 rails db:migrate
bash

By running these methods, we create a user model with name, e-mail, and password fields and have its schema migrated in the database.

The method has_secure_password must be added to the model to make sure the password is properly encrypted into the database: has_secure_password is part of the bcrypt gem, so we have to install it first. Add it to the gemfile:

1#Gemfile.rb
2gem 'bcrypt', '~> 3.1.7'
ruby

And install it:

1 bundle install
bash

With the gem installed, the method can be included in the model:

1#app/models/user.rb
2
3class User < ApplicationRecord
4 has_secure_password
5end
ruby

Encoding and Decoding JWT Tokens

Once the user model is done, the implementation of the JWT token generation can start. First, the jwt gem will make encoding and decoding of HMACSHA256 tokens available in the Rails application. First:

1 gem 'jwt'
ruby

Then install it:

1bundle install
bash

Once the gem is installed, it can be accessed through the JWT global variable. Because the methods that are going to be used to require encapsulation, a singleton class is a great way of wrapping the logic and using it in other constructs.

For those who are unfamiliar, a singleton class restricts the instantiation of a class to a single object, which comes in handy when only one object is needed to complete the tasks at hand.

1# lib/json_web_token.rb
2
3class JsonWebToken
4 class << self
5   def encode(payload, exp = 24.hours.from_now)
6     payload[:exp] = exp.to_i
7     JWT.encode(payload, Rails.application.secrets.secret_key_base)
8   end
9
10   def decode(token)
11     body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
12     HashWithIndifferentAccess.new body
13   rescue
14     nil
15   end
16 end
17end
ruby

The first method, encode, takes three parameters -- the user ID, the expiration time (1 day), and the unique base key of your Rails application -- to create a unique token.

The second method, decode, takes the token and uses the application's secret key to decode it.

Here are the two cases in which these methods will be used:

  • For authenticating the user and generating a token for him/her using encode.
  • To check if the user's token appended in each request is correct by using decode.

To make sure everything will work, the contents of the lib directory have to be included when the Rails application loads.

1    #config/application.rb
2module ApiApp
3  class Application < Rails::Application
4    #.....
5    config.autoload_paths << Rails.root.join('lib')
6    #.....
7    end
8   end
ruby

Authenticating Users

Instead of using private controller methods, simple_command can be used. For more information about installation, check out the article simple_command.

The simple command gem is an easy way to create services. Its role is similar to the role of a helper, but it instead facilitates the connection between the controller and the model, rather than the controller and the view. In this way, we can shorten the code in the models and controllers.

Add the gem to your Gemfile:

1gem 'simple_command'
ruby

And bundle it:

1bundle install
bash

Then, the alias methods of the simple_command can be easily used in a class by writing prepend SimpleCommand. Here is how a command is structured:

1class AuthenticateUser
2   prepend SimpleCommand
3
4  def initialize()
5   #this is where parameters are taken when the command is called
6  end
7
8  def call
9   #this is where the result gets returned
10  end
11
12end
ruby

The command takes the user's e-mail and password then returns the user, if the credentials match. Here is how this can be done:

1# app/commands/authenticate_user.rb
2
3class AuthenticateUser
4  prepend SimpleCommand
5
6  def initialize(email, password)
7    @email = email
8    @password = password
9  end
10
11  def call
12    JsonWebToken.encode(user_id: user.id) if user
13  end
14
15  private
16
17  attr_accessor :email, :password
18
19  def user
20    user = User.find_by_email(email)
21    return user if user && user.authenticate(password)
22
23    errors.add :user_authentication, 'invalid credentials'
24    nil
25  end
26end
ruby

The command takes the parameters and initializes a class instance with email and password attributes that are accessible within the class. The private method user uses the credentials to check if the user exists in the database using User.find_by_email.

If the user is found, the method uses the built-in authenticate method. This method can be available by putting has_secure_password in the User model to check if the user's password is correct. If everything is true, the user will be returned. If not, the method will return nil.

Checking User Authorization

The token creation is done, but there is no way to check if a token that's been appended to a request is valid. The command for authorization has to take the headers of the request and decode the token using the decode method in the JsonWebToken singleton.

A refresher on headers:

Http requests have fields known as headers. Headers can contain a wide variety of information about the request that can be helpful for the server interpreting the request. For example, a header can contain the format of the request body, authorization information, and other meta information (you can find all the types here). Tokens are usually attached to the 'Authorization' header.

Here is how the code is structured:

1# app/commands/authorize_api_request.rb
2
3class AuthorizeApiRequest
4  prepend SimpleCommand
5
6  def initialize(headers = {})
7    @headers = headers
8  end
9
10  def call
11    user
12  end
13
14  private
15
16  attr_reader :headers
17
18  def user
19    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
20    @user || errors.add(:token, 'Invalid token') && nil
21  end
22
23  def decoded_auth_token
24    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
25  end
26
27  def http_auth_header
28    if headers['Authorization'].present?
29      return headers['Authorization'].split(' ').last
30    else
31      errors.add(:token, 'Missing token')
32    end
33    nil
34  end
35end
ruby

This code executes a chain of methods. Let's start from the bottom and continue to the top.

http_auth_header

The last method in the chain, http_auth_header, extracts the token from the authorization header received in the initialization of the class.

decoded_auth-token

The previous method in the chain is decoded_auth_token, which decodes the token received from http_auth_headerand retrieves the user's ID.

user

The logic in the user method might seem abstract, so let's go through it line by line.

In the first line, the ||= operator is used to assign @user by assigning "if not nil". Basically, if the User.find() returns an empty set or decoded_auth_token returns false, @user will be nil.

Moving to the second line, the user method will either return the user or throw an error. In Ruby, the last line of the function is implicitly returned, so the command ends up returning the user object.

Implementing Helper Methods into the Controllers

All the logic for handling JWT tokens has been laid down. It is time to implement it in the controllers and put it to actual use. The two most essential pieces to implement are identifying user log-in and referencing the current user.

Login Users

First, let's start with the user's login:

1# app/controllers/authentication_controller.rb
2
3class AuthenticationController < ApplicationController
4 skip_before_action :authenticate_request
5
6 def authenticate
7   command = AuthenticateUser.call(params[:email], params[:password])
8
9   if command.success?
10     render json: { auth_token: command.result }
11   else
12     render json: { error: command.errors }, status: :unauthorized
13   end
14 end
15end
ruby

The authenticate action will take the JSON parameters for email and password through the params hash and pass them to the AuthenticateUser command. If the command succeeds, it will send the JWT token back to the user.

Let's put an endpoint for the action:

1  #config/routes.rb
2  post 'authenticate', to: 'authentication#authenticate'
ruby

Authorizing Requests

To put the token to use, there must be a current_user method that will 'persist' the user. In order to have current_user available to all controllers, it has to be declared in the ApplicationController:

1#app/controllers/application_controller.rb
2class ApplicationController < ActionController::API
3 before_action :authenticate_request
4  attr_reader :current_user
5
6  private
7
8  def authenticate_request
9    @current_user = AuthorizeApiRequest.call(request.headers).result
10    render json: { error: 'Not Authorized' }, status: 401 unless @current_user
11  end
12end
ruby

By using before_action, the server passes the request headers (using the built-in object property request.headers) to AuthorizeApiRequest every time the user makes a request. Calling result on AuthorizeApiRequest.call(request.headers) is coming from SimpleCommand module where it is defined as attr_reader :result. The request results are returned to the @current_user, thus becoming available to all controllers inheriting from ApplicationController.

Does It Work?

Let's see how everything works. Start the Rails console in the application's root directory:

1rails c
bash

Create a user and insert it into the console:

1User.create!(email: '[email protected]' , password: '123123123' , password_confirmation: '123123123')
bash

To see how authorization works, there needs to be a resource that has to be requested. Let's scaffold a resource. In your terminal, run:

1rails g scaffold Item name:string description:text
bash

This will create a resource named Item from top to bottom -- a model, a controller, routes, and views. Migrate the database:

1rails db:migrate
bash

Now, start the server and use cURL to post the credentials to localhost:3000/authenticate. Here is how the request should look:

1$ curl -H "Content-Type: application/json" -X POST -d '{"email":"[email protected]","password":"123123123"}' http://localhost:3000/authenticate
bash

Your token will now be returned.

1{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA"}
bash

Great! A token has been generated. Let's check if the resource is reachable. You can do it by making a GET request to localhost:3000/items:

1$ curl http://localhost:3000/items
2{"error":"Not Authorized"}
bash

The resource is not reachable because the token has not been prepended to the headers of the request. Copy the previously generated token and put it in the Authorization header:

1$ curl -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA" http://localhost:3000/items
2[]
bash

With the token prepended, an empty array ([]) is returned. This is normal -- after you add any items, you will see them returned in the request.

Awesome! Everything works.

If you missed something, the project has been uploaded on GitHub. If you have any questions, please feel free to message me on Github.