Building a Ruby on Rails (RoR) API Part VI: Token-Based Authentication and Security Concerns

Authentication and Security

  ·   8 min read

When I was building this API project I got a lot of recommendations to use ‘devise’. I checked the project, but to me, it was simply too much. It was like having an F1 in a rink. However I gave it a try just to be sure I was not overreacting. I wanted to set token based authentication, but for the version I checked it seemed they removed that support. After trying to make it work, I gave up, I told myself there should be an easier way to implement it. Then, I found a great article, written by Jon McCartie. There, he explains an easy way to implement it. I only had to adapt it to my specific situation, and here’s the result.

Token-Based API Authentication

In previous articles I’ve told I’m not treating the User model as an abstract one. The question now is, why? And the reason has to do with the fact it was going to be a problem to deal with authentications for both Producers and Warehouses. Now, if you think about it, it does make sense to delegate that issue to the User model itself. That’s why I’ve chosen an inheritance relationship association, in order to take advantage of it.

Preparing the User model.

The first step towards the API Token-based authentication is to prepare the User model to hold tokens. To do it so we have to add a new migration that to include the token for each user and a date to tell when the token was created. The idea is to have tokens being valid not only because of the combination of user’s email and password, but for a max lifetime for each token.

1# /db/migrate/20170502140640_add_auth_token_to_users.rb
2class AddAuthTokenToUsers < ActiveRecord::Migration[5.1]
3  def change
4    add_column :users, :auth_token, :string
5    add_column :users, :token_created_at, :datetime
6    add_index :users, [:auth_token, :token_created_at]
7  end
8end

Run the migration via rake db:migrate, to have this changes effectively applied.

Adapting the base controller

Since the base controller tells how inherited controllers will behave in a global manner, here we can set the protection for them setting a couple of methods. Here’s where you have to organize how your methods will interact with the public, surely you will allow the public usage of certain methods, and the rest of them only will work if your users are properly authenticated. No matter how that organization happens, here is where you tell your controllers how to authenticate.

 1# /app/controllers/application_controller.rb
 2class ApplicationController < ActionController::API
 3  include ActionController::HttpAuthentication::Token::ControllerMethods
 4  include Concerns::Response
 5  include Concerns::ExceptionHandler
 6
 7  before_action :require_login!
 8
 9  helper_method :user_signed_in?, :current_user
10
11  def require_login!
12    return true if authenticate_token
13    json_response({errors: [{detail: 'Access denied'}]}, :unauthorized)
14  end
15
16  def user_signed_in?
17    current_user.present?
18  end
19
20  def current_user
21    @_current_user ||= authenticate_token
22  end
23
24  private
25  def authenticate_token
26    authenticate_with_http_token do |token, options|
27      User.where(auth_token: token).where('token_created_at >= ?', 1.month.ago).first
28    end
29  end
30end

As you can see, before allowing to execute any action (i.e. execute any controller method), the require_login! is invoked. It calls the authenticate_token method, if the user is not properly authenticated, then we have to return an error object with the unauthorized http response. Otherwise it means the user is properly authenticated and they can execute any allowed controller method.

What the authenticate_token does is to rely on the built-in authenticate_with_http_token method, which automatically checks the Authorization request header for a token and passes it as an argument to the upcoming block. Then it will find the user by their token (passed by the built-in method) and check if the token creation date has less than a month of being created.

Then we have two helper methods we can use all around: current_user and user_signed_in?.

Signing in & Signing out

Handling sessions is something not hardly related to the API versioning itself. No matter how many versions of your API you have, it’s not likely to change the way you get your users logged in and out. Thus, the routing for the sign-in and sign-out goes as follows in a newer version of the routing file.

 1# /config/routes.rb
 2Rails.application.routes.draw do
 3  ...
 4
 5  # API Definition
 6  scope module: 'api' do
 7    post '/sign-in', to: 'sessions#create'
 8    delete '/sign-out', to: 'sessions#destroy'
 9
10    namespace :v1 do
11      ...
12    end
13  end
14end

Also, the folders and files’ structure will be like this:

 1.
 2└── app
 3    └── controllers
 4        ├── api
 5        │   ├── sessions_controller.rb
 6        │   └── v1
 7        │       ├── containers_controller.rb
 8        │       └── ...
 9        ├── application_controller.rb
10        └── concerns

Let’s check how does the SessionController looks.

 1# /app/controllers/api/sessions_controller.rb
 2module Api
 3  class SessionsController < ApplicationController
 4    skip_before_action :require_login!, only: [:create]
 5    def create
 6      resource = User.find_by(email: params[:user_login][:email])
 7      return invalid_login_attempt unless resource
 8
 9      if resource.valid_password?(params[:user_login][:password])
10        auth_token = resource.generate_auth_token
11        json_response({token: auth_token, userType: resource.type.to_s.downcase})
12      else
13        invalid_login_attempt
14      end
15    end
16
17    def destroy
18      resource = current_user
19      resource.invalidate_auth_token
20      head :ok
21    end
22
23    private
24    def invalid_login_attempt
25      json_response({errors: [{detail: 'Error with your login or password'}]}, :unauthorized)
26    end
27  end
28end

This controller has two methods: create and destroy. Of course, I have to avoid asking for the authentication if I’m about to authenticate, so I’m skipping the require_login! for the create method.

It delegates to the User model the logic to find the user based on the email and validate if the password is correct. Then I’m answering with the token and the user type in the response. Otherwise I sent an unauthorized http response for that combination of user and password.

The destroy method uses the helper methods defined in the ApplicationController to get the current_user and invalidate the current token.

Let’s see how does the User model looks.

 1# /app/models/user.rb
 2class User < ApplicationRecord
 3  include Localizable
 4
 5  validates_presence_of :username
 6  validates_presence_of :password
 7  validates_presence_of :email
 8
 9  def generate_auth_token
10    token = SecureRandom.hex
11    self.update_columns(auth_token: token, token_created_at: Time.zone.now)
12    token
13  end
14
15  def invalidate_auth_token
16    self.update_columns(auth_token: nil, token_created_at: nil)
17  end
18
19  def valid_password?(psswrd)
20    self.password == psswrd
21  end
22
23  def destroy
24    self.places.update_all(show: false)
25    update_attribute(:auth_token, nil)
26    super
27  end
28end

generate_auth_token and invalidate_auth_token basically generates tokens randomly updating the field in the DB, and deletes that token out of the DB respectively.

valid_password? checks if the password matchs (I know I’m using a super secure password checking strategy here ). Here’s an example of how the sign in request does work:

POST /sign-in HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 76
Content-Type: application/json

{
    "user_login": {
        "email": "user@provider.com",
        "password": "SuperSecurePassword"
    }
}

HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"814f495c575891543cbe418b8df5d03a"
Transfer-Encoding: chunked
Vary: Origin
X-Request-Id: 6e90a042-9392-412b-bc2f-aa3694ca046b
X-Runtime: 0.013186

{
    "token": "b782494fb658ba74b07f414f51fd1008",
     "userType": "producer"
}

Let’s check how the sign-out request works:

DELETE /sign-out HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 0
authorization:  Token token=b782494fb658ba74b07f414f51fd1008



HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/html
Transfer-Encoding: chunked
Vary: Origin
X-Request-Id: 2475739c-f7f8-4f69-85f7-1ec743e23d5d
X-Runtime: 0.271955

Security concerns

If you recall the original routes.rb file, you will notice I used the built-in resources word. So I could get information about any Producer no matter if that one was the Producer I was logged in via http :3000/v1/producers/2 (suppose the logged Producer had the id #5). It doesn’t feel right to talk about the producers resource when you have just implemented the authentication methodology. producers? Nah… Fortunatelly Rails offers a reserved word for that: resource (read again, it’s singular, not plural). Let’s check the file.

 1# /config/routes.rb
 2Rails.application.routes.draw do
 3  # API Definition
 4  scope module: 'api' do
 5    post '/sign-in', to: 'sessions#create'
 6    delete '/sign-out', to: 'sessions#destroy'
 7
 8    namespace :v1 do
 9      ...
10      resources :products, only: [:index, :show]
11
12      resource :producer, concerns: [:localizable, :user, :user_crops]
13      resource :warehouse, concerns: [:localizable, :user]
14    end
15  end
16end

Advisedly, I’ve let three resources here. Check that for products I’ve used resources and it is plural. This generates these routes:

1$ rake routes
2...
3 v1_products GET    /v1/products(.:format)                               api/v1/products#index
4  v1_product GET    /v1/products/:id(.:format)                           api/v1/products#show
5...

Nonetheless, check that for producer and warehouse I’m using the reserved word resource, and they in turn are singular. So they, and any nested routing, will be like:

 1$ rake routes
 2...
 3        v1_producer_places GET    /v1/producer/places(.:format)                        api/v1/places#index
 4                           POST   /v1/producer/places(.:format)                        api/v1/places#create
 5         v1_producer_place GET    /v1/producer/places/:id(.:format)                    api/v1/places#show
 6                           PATCH  /v1/producer/places/:id(.:format)                    api/v1/places#update
 7                           PUT    /v1/producer/places/:id(.:format)                    api/v1/places#update
 8                           DELETE /v1/producer/places/:id(.:format)                    api/v1/places#destroy
 9...
10               v1_producer GET    /v1/producer(.:format)                               api/v1/producers#show
11                           PATCH  /v1/producer(.:format)                               api/v1/producers#update
12                           PUT    /v1/producer(.:format)                               api/v1/producers#update
13                           DELETE /v1/producer(.:format)                               api/v1/producers#destroy
14                           POST   /v1/producer(.:format)                               api/v1/producers#create
15...
16       v1_warehouse_places GET    /v1/warehouse/places(.:format)                       api/v1/places#index
17                           POST   /v1/warehouse/places(.:format)                       api/v1/places#create
18        v1_warehouse_place GET    /v1/warehouse/places/:id(.:format)                   api/v1/places#show
19                           PATCH  /v1/warehouse/places/:id(.:format)                   api/v1/places#update
20                           PUT    /v1/warehouse/places/:id(.:format)                   api/v1/places#update
21                           DELETE /v1/warehouse/places/:id(.:format)                   api/v1/places#destroy
22...
23              v1_warehouse GET    /v1/warehouse(.:format)                              api/v1/warehouses#show
24                           PATCH  /v1/warehouse(.:format)                              api/v1/warehouses#update
25                           PUT    /v1/warehouse(.:format)                              api/v1/warehouses#update
26                           DELETE /v1/warehouse(.:format)                              api/v1/warehouses#destroy
27                           POST   /v1/warehouse(.:format)                              api/v1/warehouses#create
28...

This makes our API smart enough to recognize what logged user are we talking about, and also protect other users sensitive information.

Conclusion

Pre-conclusion actually. I’m planning to have a fully dedicated conclusion article when finishing this series, but I’m compelled to recognize Jon McCartie’s work here. I don’t want to dismiss devise at all, maybe they are good for bigger projects or alternatives, however in Jon McCartie’s words:

As Ruby developers, we have a littany of incredible Gems available to us. And when faced with the question “Should I build this myself? Or use a gem?”, there’s usually incredible value to not re-inventing the wheel. But make sure your reliance on other libraries never blinds you to simpler solutions that can be fully customized to your needs. Sometimes “rolling your own” can actually save you time.