A complete guide for building microservices with Ruby on Rails – Part III

Assumption: You are referring to this sample API application along these series of posts.

In last post we covered Why, What and How of API versioning.

In this post we will see

  • What and why of API authentication?
  • Various strategies for API authentication with recommendations.

API authentication: What and Why?

Well if you are exposing/writing API endpoints for the application which are not public, you need to authenticate all of the API endpoints to protect and secure applications data. API authentication means allowing only authenticated requests/users/consumers to access application’s data via API’s in a secure way.

In simple words API authentication is nothing but asking an API requester to prove his identity.

What are the various ways of API authentication?

There are two popular ways for API authentication, HTTP basic authentication & API Key based authentication.

HTTP basic authentication

This is the first and most simple solution for authenticating API’s. In which an API requesters pass ‘username:password’ in request ‘Authorization’ header as follows.

GET / HTTP/1.1
Host: yourhost.com
Authorization: Basic Zm9vOmJhcg==

But this is considered as less secure way of api authenticating, as it can be compromised if HTTP communication is not secure. If you are interested in implementation you can check out how to implement HTTP basic authentication in Rails

API authentication via API key/token

This is the most commonly used and recommended way of API authentication, to implement token based API authentication we will use JWT ruby implementation.

Let’s see some of the advantages using JWT based API tokens

  • It allows you to encode and decode payload (user meta information) into a secure token (random bytes) via various cryptographic algorithms.
  • The token which you get against the payload is a computed token via secret key which makes it highly secure and unpredictable.
  • Additionally JWT allows token expiration after which payload cannot be decoded which makes it un-processable.
  • As its a computed token no database storage required.

Time to implement JWT for API authentication with Rails

First step first add jwt gem into your Gemfile and ‘bundle install’

gem 'jwt'

Writing ApiKeyHandler

#app/services/api_key_handler.rb

class ApiKeyHandler
  def self.encoded_api_key(user_id)
    payload = { exp: 4.week.from_now.to_i, user_id: user_id.to_s }
    api_key = JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

  def self.decode_api_key(api_key)
    ## Sample claims hash
    #[
    #   {
    #     "exp"=>1443275296,
    #     "user_id"=>"55e1b72e84f16d427a000000"
    #   },
    #  {"typ"=>"JWT", "alg"=>"HS256"}
    #]
    JWT.decode(api_key, Rails.application.secrets.secret_key_base)
  end
end

If you have noticed ApiKeyHandler has two methods for encoding and decoding payload data which contains

  1. All the information from which we can pull out the users rest of the data.
  2. Expiration time of token.

ApiKeyHandler#encode which uses JWT#encode method which takes a payload with expiration time, a secret key for encoding and returns encoded api_key.

Similarly, ApiKeyHandler#decode which uses JWT#decode method takes encoded api_key, a secret key and returns a decoded payload data.

Using ApiKeyHandler

#app/models/user.rb

class User 
  ...
  def generate_api_key
    ApiKeyHandler.encoded_api_key(self.id)
  end
  ...
end

Returning user api key via login api

#app/controllers/v1/users_controller.rb

class V1::UsersController < V1::BaseController
  skip_before_action :authenticate!, only: [:login]
  ...
  def login
    user_params = params[:user]
    user = User.where(email: user_params[:email]).first
    unless user and user.valid_password?(user_params[:password])
      render json: { message: "Invalid credentials"}, status: 401
    else
      render json: { email: user.email, api_key: user.generate_api_key }
    end
  end
  ...
end

Note: To explain first time generation and returning of encoded api_key in response, I have briefed only login method here, to see the full implementation of user create and login flow please visit a sample application.

Did you noticed? We are skipping, authentication via skip_before_action :authenticate!, why? Left as an exercise :p.

Make login request to get a api_key

curl -X POST \
  http://localhost:3000/users/login \
  -H 'Accept: application/vnd.expense-manager.com; version=1' \
  -H 'Content-Type: application/json' \
  -d '{"user":{"email":"test0@domain.com","password":"test123"}}'

Response

{
    "email": "test0@domain.com",
    "api_key": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NTAwNTU1NjAsInVzZXJfaWQiOiI1YjYxOTUwMzM4YzcyZDM2ZjI5ZmM4MDkifQ.DQN3jYq7gHn71cuEaKodywIGYH9sm0w6Q7Zz8yJ0mvY"
}

Authenticating user request via API key

#app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  before_action :authenticate!
  X_API_KEY = 'X-Api-Key'
  ...
  def authenticate!
    set_current_user || render_unauthorized
  end

  def set_current_user
    return false unless api_key?
    claims = ApiKeyHandler.decode_api_key(request.headers[X_API_KEY])
    @current_user = User.find(claims[0]['user_id'])
  rescue JWT::ExpiredSignature
    render_expired_message
  rescue JWT::DecodeError
    render_decode_error
  end

  def api_key?
    request.headers[X_API_KEY].present?
  end  
  ...
end

Note: Check out the full implementation.

Our goal is to authenticate each an every request via verifying api_key, so we need to add a ‘before_action :authenticate! method in ApplicationController. In this method we are trying to decode api_key which is passed in request header using ApiKeyHandler, on success we are setting a current user otherwise render an error response.

curl -X GET \
  http://localhost:3000/users/expenses \
  -H 'Accept: application/vnd.expense-manager.com; version=1' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NTAwNTY5NDYsInVzZXJfaWQiOiI1YjYxOTUwMzM4YzcyZDM2ZjI5ZmM4MDkifQ.ImCJMzOJh79_l8H53QEJ2W_S3kOouBmLx4I9wSqiTA4'

Note: Here we are passing api key as a ‘X-Api-Key’ in request header.

That’s it. We have seen how to generate and use JWT token for API authentication, we can conclude the part III of this series and see you soon with the next post on API throttling i.e. rate limiting.

If you have any queries please post in comments section and keep exploring!

Advertisements

Solving 10 most common issues of Ruby on Rails

Being a Ruby on Rails developer for last 5 years, I realised that newbie/fellow developers who just started learning Ruby on Rails struggles with many issues which are pretty common and obvious. This post takes you through the most common issues and their resolution while learning RoR development.

Let’s see the most common issues that occur while application development.

1. PostgresQL: Can’t find the ​​’libpq-fe.h’ header while installing pg gem.

Building native extensions. This could take a while...
ERROR:  Error installing pg:
ERROR: Failed to build gem native extension.Can't find the 'libpq-fe.h header
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of
necessary libraries and/or headers.  
Check the mkmf.log file for more
details.  You may need configuration options.
Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/home/u/.rvm/rubies/ruby-1.9.2-p0/bin/ruby
    --with-pg
    --without-pg
    --with-pg-dir
    --without-pg-dir
    --with-pg-include
    --without-pg-include=${pg-dir}/include
    --with-pg-lib
    --without-pg-lib=${pg-dir}/lib
    --with-pg-config
    --without-pg-config
    --with-pg_config
    --without-pg_config

Above error occurs while `gem install pg` or  `bundle install` with pg gem. It says that gem could not find required packages for successful installation.

Solution:

Installing required packages: pg gem needs libpq-dev package which can be installed as follows

Ubuntu: sudo apt-get install libpq-dev
Mac: brew install postgresql (with Homebrew)

Or install gem with include directory: if package is already installed and gem install is not able to locate it.

gem install pg -- --with-pg-include=path/to/postgres/include/directory 
Mac OS - include directory usually located in /usr/local/opt i.e. /usr/local/opt/postgresql\@9.5/include/
Ubuntu OS - include/config directory usually located in /etc/postgresql/9.x/*

2. PostgreSQL:  FATAL: Peer authentication failed for user “postgres”

Above error is because of database user postgres(default user) can’t be authenticated with the password provided in config/database.yml.

Solution:

Either you need to set the password for ‘postgres’ user or allow password less authentication for ‘postgres’ user from the localhost. You can do this by editing `pg_hba.conf`, which is located in following locations
Ubuntu : /etc/postgresql/9.x/main,
Mac : /usr/local/var/postgres/pg_hba.conf

Allowing password less authentication:  Change following line in `pg_hba.conf`

# TYPE  DATABASE        USER          ADDRESS           METHOD
local   all             all           127.0.0.1/32       peer

to

local   all             all           127.0.0.1/32       trust

Restart postgres service with:

Ubuntu: sudo service postgresql restart
Mac: brew services restart postgresql

3. MySQL: Error installing mysql2: ERROR: Failed to build gem native extension.

This error occurs while ‘bundle install’ with ‘mysql2’ gem or while executing ‘gem install mysql2’ command, It is because required packages for mysql2 gem are missing.

Solution: 

Installing required packages for mysql2 gem.

Ubuntu: sudo apt-get install libmysql-ruby libmysqlclient-dev 
Mac: brew install mysql

4. TCPServer Error: Address already in use – bind(2)

This error pops up when you run `rails s` command in your rails app directory, it is because you are trying to start rails server on a port (default 3000) which is already used by some other process or you have running rails server instance on same port (i.e. 3000).

Solution: 

To resolve this error, find the process that is using port 3000 (default or whichever you use) and kill the process or find the already running rails server process and kill it.

ps -ef | grep rails -> will get you the pid of running rails server  
lsof -wni tcp:3000 ->  will get you the pid of the process which is using port 3000 
sudo kill -9 pid #kill the process

5. Could not find a JavaScript runtime. See https://github.com/sstephenson/execjs,

This is the another most common error occurs because of the missing javascript runtime for your application.

Solution:

ExecJS supports various Javascript runtimes, most of the developer uses `therubyracer` as a runtime. You can install it by adding following lines into your Gemfile.

gem 'execjs' 
gem 'therubyracer', platforms: :ruby

Or to fix this problem for all the project install Node.js

sudo apt-get install nodejs

6. Unpermitted parameters

If your are working with nested attributes/parameters using accepts_nested_attributes_for this is the most common issue that you face.

Example:

class Category < ActiveRecord::Base
  belongs_to :brand
end
class Brand < ActiveRecord::Base
  has_many :categories, dependent: :destroy 
  accepts_nested_attributes_for :categories, allow_destroy: true
end

When you post the parameters with the following format.

{ 
  "utf8"=>"✓", 
  "authenticity_token"=>"sdunxlowasd=O",
  "brand"=>{"name"=>"Brand Name",
    "categories_attributes"=>{
      "0"=>{"name"=>"category name 1", "priority"=>"1"}, 
      "1"=>{"name"=>"category name 2", "priority"=>"2"},
    }
  }, 
  "commit"=>"Save"
}

Here, If you notice categories_attributes are nested inside brand. If you post these kind of parameters to controller you are likely to get following error.

Unpermitted parameters: categories_attributes

Solution:

You can permit nested parameters in controller with permit method as follows.

class BrandController < ActionController::Base
  def create
    Brand.create(brand_params)
  end

  private
  def brand_params
    params.require(:brand).permit(:name, categories_attributes: [:id, :name, :priority])
  end
end

7. No route matches “/users/sign_out”

Most of the developers uses devise gem for user authentication, when you setup all things related to devise and when you hit a ‘logout’ button first time you are likely to receive this error above message.

Solution: 

Recommended solution for this error is to add http method DELETE to the sign out link.

<%= link_to "Sign out", destroy_user_session_path, method: :delete %>

Another way to fix this issue is by updating config/initializers/devise.rb

config.sign_out_via = :delete

to

config.sign_out_via = :get

8. WARNING: Can’t verify CSRF token authenticity

When you execute AJAX requests this is most common error you get. Rails by default prevents all the request without CSRF token to protect attacks like cross-site forgery

Solution:

To resolve this issue make sure you have

  1. <%= csrf_meta_tag %> in your layout
  2. Every AJAX request sending CSRF token in request headers

You can setup all AJAX request to send CSRF token as follows

$.ajaxSetup({
  headers: {
    'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
  }
});

9. Don’t know how to build task ‘test’ (See the list of available tasks with `rake –tasks`) 

When you write the rake tasks for the first time in your rails application, this is the first error you will encounter.
Solution:
1. Make sure you save your rake task file in lib/tasks/*
2. Rake file must have `.rake` as a file extension many a time mistakenly we add `.rb` as a file extension.

10. ActionView::Template::Error (application.css isn’t precompiled)

When you visit your application for the first time after deploying it to the production, You are likely to receive above error message. Rails by default assumes that all the applications assets (jasvascripts, css etc) are pre-complied in production environment and try to find the pre-complied assets.
Solution:
Recommended way to solve this issue is, you should pre-compile Rails assets with following command.
bundle exec rake assets:precompile

You can make this command as a part of application deploy script using Mina or Capistrano.

Another way to resolve this error is by telling rails to compile assets on runtime but this is not an efficient solution which is not recommended. You can resolve this error by setting assets compile to true in config/environments/production.rb.
config.assets.compile = true

Hope you enjoyed this article! If you have any suggestions/thoughts about this post let me know via comments. Happy learnings…