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 thegem 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
endendconfig/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
endend...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
endend...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.