Build Restful APIs in a Rails 6 application with Token Authentication

  • Create a Rails 6 application
  • What is API?
  • Your first API
  • Secure your API with token authentication

In this many parts of this series, we will be creating a Rails 6 application using many of its newly introduced features to show you how it works in a real life example.

Create a Rails 6 application

For how to setup your environment on a Ubuntu, you can refer to my article “How to install Ruby On Rails and MySQL on Ubuntu 18.04”.

To install Rails 6.0, do gem install rails -v 6.0.0 after you do the gem install bundler.

Now it’s time to create a new application.

$ rails -v
Rails 6.0.1
$ rails new travel-plan

Once you are done, star the server rails s . You should be able to see the below screen on your browser.

What is API?

API stands for application programming interface. It is used to serve as an interface between the frontend and backend or between different backends. For example, if you want to accept payment on your website, you can integrate the API from Stripe. These APIs are to provide standard ways for different applications to include 3rd party functionalities. Usually these APIs have the same set of request and response format as well as a list of standard error handling. Enough of talking, it’s easier to show in a sample.

Your first API

Let’s create our first model.

$ rails g model Trip name:string
Running via Spring preloader in process 80190
invoke active_record
create db/migrate/20191118105607_create_trips.rb
create app/models/trip.rb
invoke test_unit
create test/models/trip_test.rb
create test/fixtures/trips.yml
$ rails db:migrate
== 20191118105607 CreateTrips: migrating ===============
-- create_table(:trips)
-> 0.0470s
== 20191118105607 CreateTrips: migrated (0.0473s) ======

Then we will create our first API controller for accessing this trip information.

app/controllers/api/v1/trips_controller.rbclass Api::V1::TripsController < ActionController::API  def index
trips = Trip.all
trips = trips.map do |trip|
{ id: trip.id, name: trip.name }
end

render json: { results: trips }.to_json, status: :ok
end
endconfig/routes.rbRails.application.routes.draw do

namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :trips, only: [:index]
end
end

end

Before we test our first API, let’s seed some sample data and run rails db:migrate at the terminal.

db/seeds.rbtrip_names = [  "Romantic Spots for Honeymoons", 
"Top Island Getaways",
"Most Romantic Destinations",
"Top Spots for Skiing" ]
trip_names.each do |trip_name|
Trip.create(name: trip_name)
end

Now we could check on our API and see if it works.

$ curl http://localhost:3000/api/v1/trips
{"results":[{"id":1,"name":"Romantic Spots for Honeymoons"},{"id":2,"name":"Top Island Getaways"},{"id":3,"name":"Most Romantic Destinations"},{"id":4,"name":"Top Spots for Skiing"}]}

Secure your API with token authentication

Whether you allow public access to your API, it’s best to protect your API in some form of authentication. Here we use 2 different gems called Devise and Ruby-JWT. Devise is a framework to provide authentication solution. And Ruby-JWT is the ruby implementation of a JSON Web Token standard. The token-based approach is suitable for non-session based request and in our API situation is a right use case for such scenario.

In your Gemfile, we will add the following and run bundle install. Follow the steps to install Devise.

Gemfile
gem 'devise'
gem 'jwt'
...$ rails generate devise:install
$ rails generate devise User
$ rails db:migrate

After we are done with these, we will create our “token”.

lib/json_web_token.rbclass JsonWebToken  class << self
def encode(payload, expiry = 24.hours.from_now)
payload[:expiry] = expiry.to_i
secret_key_base = Rails.application.secrets.secret_key_base
JWT.encode(payload, secret_key_base)
end

def decode(token)
secret_key_base = Rails.application.secrets.secret_key_base
body = JWT.decode(token, secret_key_base)[0]
HashWithIndifferentAccess.new body
rescue
nil
end
end
end...config/application.rb(Add the following line in the file)
config.autoload_paths << Rails.root.join("lib")

Next we will setup the controller. The application_controller.rb is our base class for all the API controllers. And authentication_controller.rb is to handle the authentication and return back the “token” to the requester.

app/controllers/api/application_controller.rbmodule Api
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_api_user

private

def authenticate_request
@current_api_user = AuthorizeApiRequest.call(request.headers)
.result

render json: { error: "This is not a authorized request." },
status: :unauthorized unless @current_api_user
end
end
end
...app/controllers/api/v1/authentication_controller.rbclass Api::V1::AuthenticationController < Api::ApplicationController

skip_before_action :authenticate_request

def authenticate
command = AuthenticateUser.call(
params[:email], params[:password])

if command.success?
render json: { auth_token: command.result }
else
render json: { error: command.errors }, status: :unauthorized
end
end

end
...config/routes.rbnamespace :api, defaults: { format: :json } do
namespace :v1 do
post 'authenticate', to: 'authentication#authenticate'
resources :trips, only: [:index]
end
end

In the code you see AuthenticateUser and AuthorizeApiRequest , these two classes are created to simplify the code between model and controller. We use a gem call SimpleCommand to do that.

Gemfile
gem 'simple_command'
...$ bundle installapp/commands/authenticate_user.rbclass AuthenticateUser
prepend SimpleCommand

def initialize(email, password)
@email = email
@password = password
end

def call
JsonWebToken.encode(user_id: api_user.id) if api_user
end

private

attr_accessor :email, :password

def api_user
user = User.find_by_email(email)
unless user.present?
errors.add :message, "Invalid email / password"
return nil
end

# Verify the password. You can create a blank method for now.
unless user.authenticate(password)
errors.add :message, "Invalid email / password"
return nil
end

return user
end

end

Now on your terminal, test this.

$ rails c
Running via Spring preloader in process 42381
Loading development environment (Rails 6.0.1)
2.6.3 :001> User.create(email: 'john.doe@example.com', password: 'test123')
...$ curl -X POST http://localhost:3000/api/v1/authenticate \
-H 'content-type: multipart/form-data' \
-F 'email=john.doe@example.com' -F 'password=test123'
{"auth_token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHBpcnkiOjE1NzQ0MTg4MTJ9.Gdq2ClrLd9iR2JUv6gETMOkkMetKP82sJ4CgGAfWlqQ"}

Yay, it works. We successfully get back our token.

Now to secure our trips_controller.rb , we will extend Api::ApplicationController . If you have look closely, we also have one more class to define AuthorizeApiRequest.

app/controllers/api/v1/trips_controller.rbclass Api::V1::TripsController < Api::ApplicationControllerdef index
trips = Trip.all
trips = trips.map do |trip|
{ id: trip.id, name: trip.name }
end

render json: { results: trips }.to_json, status: :ok
end
end...app/commands/authorize_api_request.rbclass AuthorizeApiRequest
prepend SimpleCommand

def initialize(headers = {})
@headers = headers
end

def call
api_user
end

private

attr_reader :headers

def api_user
@api_user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
@api_user || errors.add(:token, "Invalid token") && nil
end

def decoded_auth_token
@decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end

def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, "Missing token")
end
nil
end
end

We could again test the Trips API. First we will call without the token and subsequently we will make a call with the token we just got in our last curl command.

$ curl http://localhost:3000/api/v1/trips
{"error":"This is not a authorized request."}
$ curl http://localhost:3010/api/v1/trips -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHBpcnkiOjE1NzQ0MTg4MTJ9.Gdq2ClrLd9iR2JUv6gETMOkkMetKP82sJ4CgGAfWlqQ'
{"results":[{"id":1,"name":"Romantic Spots for Honeymoons"},{"id":2,"name":"Top Island Getaways"},{"id":3,"name":"Most Romantic Destinations"},{"id":4,"name":"Top Spots for Skiing"}]}

This is the framework of having the token implementation, there are a couple of things I did not go into detail and you may consider when implementing your own verison.

  • Verifying the password
  • Checking the expiry of the token
  • Caching the token

See you next time when we have our Part 2 of the Rails 6 application.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Billy Cheng

Billy Cheng

Share my tips and codes on my work with Ruby on Rails — Hong Kong