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.
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.
There are several benefits to using such an approach:
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.
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.
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.
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.
Because the application does not rely on cookies for authentication, it is invulnerable to cross-site request attacks.
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.
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:
The token is separated into three base-64 encoded, dot-separated values. Each value represents a different type of data:
Consists of the type of the token (JWT) and the type of encryption algorithm (HS256) encoded in base-64.
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 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.
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
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:
First, the user model must be created:
1 rails g model User name email password_digest
Run the migrations:
1 rails db:migrate
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'
And install it:
1 bundle install
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
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'
Then install it:
1bundle install
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
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:
encode
.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
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'
And bundle it:
1bundle install
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
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
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
.
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.
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
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'
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
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
.
Let's see how everything works. Start the Rails console in the application's root directory:
1rails c
Create a user and insert it into the console:
1User.create!(email: '[email protected]' , password: '123123123' , password_confirmation: '123123123')
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
This will create a resource named Item
from top to bottom -- a model, a controller, routes, and views. Migrate the database:
1rails db:migrate
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
Your token will now be returned.
1{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA"}
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"}
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[]
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.