Though using Turbolinks3 is the fastest way to build SPA like web pages in Rails, everybody is running after JS frameworks like React, EmberJS, etc these days. Another use case of Web-APIs would be back-end for Mobile application.

We normally build web services for Authentication, Authorization, Skimming up mobile apps, etc. Mobile phones are supposed to be dumb as server machines are much more powerful, so to leverage the server power we normally do heavy jobs in server. Also, we don’t trust mobile phones so we put Authorization/Authentication in servers.

Authentication

A process of identifying who you are talking to

Steps During Sign Up

Generate a new user with email and password_digest field

Since we are not using Devise which would be useless and overkill in this context, we would need a mechanism to store user’s sensitive information like passwords safely. We will use has_secure_password method available in ActiveModel.

We now generate a model called User using the following command:

  $ rails g model User first_name last_name email password_digest tokens:text
  Running via Spring preloader in process 910
        invoke  active_record
        create    db/migrate/20170710045726_create_users.rb
        create    app/models/user.rb
        invoke    rspec
        create      spec/models/user_spec.rb

Then we create database and run the migrations

  $ rails db:create
  $ rails db:migrate

We will now define controller and routes for APIs to signup.

  $ rails g controller api/v1/users
  
  Running via Spring preloader in process 3223
  create  app/controllers/api/v1/users_controller.rb
  invoke  rspec
  create    spec/controllers/api/v1/users_controller_spec.rb

Now we define routes to api/v1/users_controller

  # config/routes.rb
  Rails.application.routes.draw do
    namespace :api do
      namespace :v1 do
        resources :users
      end
    end
  end

Now you can make Post calls to create action but nothing will happen. You will need to write code to handle genuine requests. So, we setup strong params in users_controller like below to secure our database. Only accepting safe fields from user-side.

  def users_params
    params.permit(:email, :first_name, :last_name, :password, :password_confirmation)
  end

Note:
When you make post requests with sensitive fields like password, Rails automatically masks the sensitive information in the logs for privacy purposes. See this for more info http://edgeguides.rubyonrails.org/security.html#logging

Processing by Api::V1::UsersController#create as */*
  Parameters: {"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "first_name"=>"Shiva", "last_name"=>"Bhusal", "email"=>"shiva@example.com"}  

You might wonder, we did not define fields related to password while generating models but they are in strong params

We don’t have to if we use has_secure_password in our model. See below:-

has_secure_password

This method is called in Authenticable models(say User) in class-scope to make methods/attributes like password, password_confirmation and authenticate available though they are not database fields. The former one is used to check if the password provided matches with password associated with that particular User object.

In addition, it will also set a validation to check if password and password_confirmation fields match before it is saved. To use this feature you need to have a field called password_digest, where the password hash will be stored. Some validations come along for free.

  # Schema: User(name:string, password_digest:string)
  class User < ActiveRecord::Base
    has_secure_password
  end

  user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
  user.save                                                       # => false, password required
  user.password = 'mUc3m00RsqyRe'
  user.save                                                       # => false, confirmation doesn't match
  user.password_confirmation = 'mUc3m00RsqyRe'
  user.save                                                       # => true
  user.authenticate('notright')                                   # => false
  user.authenticate('mUc3m00RsqyRe')                              # => user
  User.find_by(name: 'david').try(:authenticate, 'notright')      # => false
  User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user

Note:
By default bcrypt gem is commented in your Gemfile. Uncomment it to use the has_secure_password feature.

  # Use ActiveModel has_secure_password
  gem 'bcrypt', '~> 3.1.7'
  
  # and re-bundle your app
  $ bundle install

Also we will use ActiveModel::Serializer to easily serialize our Model data to JSON string.

  # Gemfile
  gem 'active_model_serializers', '~> 0.10.0'
  
  # and re-bundle your app
  $ bundle install 

By default, it will use json adapter but if you wish you can have json_api adapter complying with jsonapi.org’s specification. We will be using json_api adapter. So we configure accordingly

  # config/serializer_config.rb
  ActiveModelSerializers.config.adapter = :json_api

Tips:
Traditionally we used respond_to method that used to come by default with ActionController for rendering responses according to MIME type requested by the client. But, these days, ActionController::API does not include it out of the box. It has been moved to responders gem. See this for more info:

We create serializer for our User model.

  $ rails g serializer User
  
  # rails g serializer User
  # Running via Spring preloader in process 25912
  #    create  app/serializers/user_serializer.rb

Some adjantages of using serializers

  • You can easily choose which fields you want to be present in you API response
  • It supports model-associations like has_many, has_one
  • It acts as a presenter/decorator layer, where you can munge your data a little bit before you serve
  • You can add some extra non-model fields/attributes
  • It does auto json keys conversion like first_name appears to be first-name in response.

Lets define our attributes in the serializer

  class UserSerializer < ActiveModel::Serializer
    attributes :id, :first_name, :last_name, :email
  end

then our controller looks like

  class Api::V1::UsersController < ApplicationController
    def create
      user = User.new(users_params)
      if user.save
        render json: user, status: :ok # or 200
      else
        render json: user.errors, status: :bad_request # or 400
      end
    end
    
    private
      def users_params
        params.permit(:email, :first_name, :last_name, :password, :password_confirmation)
      end
  end

You can use Ggoogle Chrome’s Postman app that can run independent of the browser to make API calls. Its an amazing tool we can get for free.

# For such request
Processing by Api::V1::UsersController#create as */*
  Parameters: {"first_name"=>"Shiva", "last_name"=>"Bhusal", "email"=>"shiva@example.com", "password"=>"password", "password_confirmation"=>"password"}

Response:
Status code 200

  {
      "data": {
          "id": "1",
          "type": "users",
          "attributes": {
              "first-name": "Shiva",
              "last-name": "Bhusal",
              "email": "shiva@example.com"
          }
      }
  }

Tip:
We are missing confirmation part. You will need to create the user and send a confirmation email to the user with a link to your api/v1/confirmation_controller#create. You will have to generate a unique confirmation token per user and embed in the link you send via email. When user clicks on the link we identify the user and confirm the object.
If you request I can add confirmation part as well. Please comment below.

Steps During Sign In

After we make API to register a new user, now we should let users to sign in. Here comes our Authentication mechanism. We will first authenticate the user with email and password, and then, we will give back the 200 response with a newly generated token.

Verify the User’s creds with database value

For authentication we will create sessions_controller. Sessions controller will issue unique tokens to active sessions and invalidate if required. The tokens have expire date associated with them. With every request while authenticating the token this parameter shall be questioned as well.

  class Api::V1::SessionsController < ApplicationController
    def create
      user = User.find_by_email(params[:email])
      if user.authenticate(params[:password])
        token = user.generate_token
        render json: token, status: :ok
      else
        render json: { error: "Wrong email or password" }, status: :unauthorized # or 401
      end
    end
  end

Corresponding routes would be

  Rails.application.routes.draw do
    namespace :api do
      namespace :v1 do
        resources :users

        post   :signin,  to: 'sessions#create'
        delete :signout, to: 'sessions#destroy'

      end
    end
  end

and in User model we will do like

  class User < ApplicationRecord
    has_secure_password
    validates_uniqueness_of :email
    serialize :tokens, Array
    
    def generate_token
      token = SecureRandom.base64(64)
      
      while User.where("id = #{self.id} and tokens like ?", "%#{token}%").present? do
        token = SecureRandom.base(64)
      end
      
      token_object = {token: token, created_at: Time.now.utc, expires_at: 10.days.from_now.utc}
      self.tokens << token_object
      self.save
      token_object
    end
  end

Note:
Here we are using Tokens field as array. The array is implemented by Rails in a tricky way. While storing Rails serializes the object and after retreval it de-serializes the object to native ruby object. Keep in mind that objects you stores should be Deserializable.

Tokens will be stored like

[
 {"token"=>"QZ5HoTliBMHcX7BePAzdpU0S2V3XvSLYwoUW+vYFMu0VUaY2ONydXHLjwToOMysPuWkdZ2hUQeWJGIsFfqXrhg==",
  "created_at"=>2017-07-10 18:21:32 UTC,
  "expires_at"=>Thu, 20 Jul 2017 12:36:32 UTC},
 {"token"=>"TutfyMhiUqKo5tAgpXWxwSeeouY19Y2nmRwJWNvua4P+f7KqrkKiGV99iDZJJX6qBKBp0j9UuvZpPC7Q8BKsFw==",
  "created_at"=>2017-07-10 18:22:09 UTC,
  "expires_at"=>2017-07-20 12:37:09 UTC},
 {"token"=>"W66WZmvYm3rCnKI5RO5jmQuesjCaxf7g8rwMuTU1A3nhPNSMKEQ0OSszagQ+s9YZd0cZO3TF4m6vjqe/9gEMiQ==",
  "created_at"=>2017-07-10 18:22:11 UTC,
  "expires_at"=>2017-07-20 12:37:11 UTC},
 {"token"=>"zw+HrUdrBez+2yiRyTudJ4QnxKI3z/ApSZokskgFZexR2Amrn3S9h1ElWm5jwpT+ehLPZsP50aPA6Ep4YyMxzQ==",
  "created_at"=>2017-07-10 12:37:26 UTC,
  "expires_at"=>2017-07-20 12:37:26 UTC}
]

Signin request is like

  Started POST "/api/v1/signin" for 127.0.0.1 at 2017-07-10 18:22:26 +0545
  Processing by Api::V1::SessionsController#create as */*
    Parameters: {"first_name"=>"Shiva", "last_name"=>"Bhusal", "email"=>"shiva@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}

Response we get upon successful authentication is

  {
      "token": "zw+HrUdrBez+2yiRyTudJ4QnxKI3z/ApSZokskgFZexR2Amrn3S9h1ElWm5jwpT+ehLPZsP50aPA6Ep4YyMxzQ==",
      "created_at": "2017-07-10T12:37:26.224Z",
      "expires_at": "2017-07-20T12:37:26.224Z"
  }

You can send such request using tools like curl or Postman.

Shall support multi-devise sign-ins

Multiple tokens associated to a user denotes that it can keep track of multiple logins via different devices. For more info you can add extra fields like ip-address, user-agent, etc in the token hash.

Verify that token and expiry date-time in every request

For this we will need some mechanism to verify the tokens if they are generated by us for that particular user. So, user/client is required to send email and token with every request to resources.

also we need to lets say we will let a user/client to update his/her information; for that purpose we need to make sure its him/her.

To secure every request we need to intercept it and check for a valid token for the user’s ID. For that purpose, we will build some before_action filters like

You will have to send token and email in HTTP header like

 header 'Authorization', Token zw+HrUdrBez+2yiRyTudJ4QnxKI3z/ApSZokskgFZexR2Amrn3S9h1ElWm5jwpT+ehLPZsP50aPA6Ep4YyMxzQ==  shiva@example.com
  class ApplicationController < ActionController::API
    attr_reader :current_user
    private
    
    def authenticate_user!
      _, token, email = request.headers['Authorization'].split(' ')
      user = User.where("email = ? and tokens like ?", email, "%#{token}%").first
      if user.present? && user.has_valid_token?(token)
        @current_user = user
      else
        render json: {error: 'Invalid Authorization token'}, status: :unauthorized
      end
    end
    
  end
  # in models/user.rb

  def has_valid_token?(token)
    self.tokens.each do |stored_token|
      return true if stored_token['token'] == token && stored_token['expires_at'] > Time.now.utc
    end
    false
  end

# See this for codes https://dev.twitter.com/overview/api/response-codes
  class Api::V1::UsersController < ApplicationController
    before_action :authenticate_user!, only: :update
    before_action :authorize_self, only: :update
    def create
      user = User.new(users_params)
      if user.save
        render json: user, status: :ok # or 200
      else
        render json: user.errors, status: :bad_request, adapter: :json_api # or 400
      end
    end
    
    def update
      if current_user.update(users_params)
        render json: current_user.reload, status: :ok
      else
        render json: current_user.errors, status: 406 # or Not Acceptable
      end
    end
    
    private
      def users_params
        params.permit(:email, :first_name, :last_name, :password, :password_confirmation)
      end
      
      def authorize_self
        unless current_user.id.to_s == params[:id]
          render json: {error: 'You are not authorized to perform the action'}, status: :forbidden # or 403
        end
      end
  end

this way, requests like following can be processed well

Started PATCH "/api/v1/users/1" for 127.0.0.1 at 2017-07-10 21:54:04 +0545
Processing by Api::V1::UsersController#update as */*
  Parameters: {"first_name"=>"bhusal", "last_name"=>"Bhusal", "email"=>"shiva@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "id"=>"1"}

Response

  {
      "data": {
          "id": "1",
          "type": "users",
          "attributes": {
              "first-name": "bhusal",
              "last-name": "Bhusal",
              "email": "shiva@example.com"
          }
      }
  }

User will have to send token and email every time.
You can even make users to send uid instead of email.

  • uid would be a random string uniquely generated for that email. Alternatively, it can be a hash of that email with password as salt.

You can take this implementation further more. Please recommend if you think its helpful and suggest anything via comments below.

!!! HAPPY CODING !!!


Tags

Ruby  Rails  Authentication  Authorization  WebAPI  Rails API  No Devise