Codementor Events

Simple OTP Authentication with Rails API and Twilio - PART II

Published Sep 09, 2020
Simple OTP Authentication with Rails API and Twilio - PART II

From our previous article, we looked at how to integrate the Twilio service with our Rails API project leveraging on Sidekiq and Redis. In this article, we would be creating the following:

  • An Authentication/Verification service.
  • A worker file for handling our background process.
  • An send otp code route to test on Postman.
  • Integrating Rspec and writing test for our authentication service.

Let's get started. To begin, open the rails project from the last article. twilio_otp_authentication.
Next, let's add the service SID, Account SID and Auth token to our .env file created in the last article. The .env file should look like this:

REDIS_URL=redis://127.0.0.1:6379/0
AUTH_TOKEN=auth_token_from_project_console
SERVICE_SID=service_sid_from_verify_service_page
ACCOUNT_SID=account_sid_from_project_console

Next, let's integrate RSpec to our project. To do that, we would add rspec-rails and factory_bot_railsgems to the development and test section of our Gemfile. Our Gemfile would look like this:

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails', '~> 3.9'
  gem 'factory_bot_rails', '~> 5.1', '>= 5.1.1'
end

Next, run bundle install

bundle install

Next, run the following on your terminal to generate the rspec and rails helper files

rails generate rspec:install

Next, go to the rspec folder and create a new folder called support.
Next, create a file factory_bot.rb in the support folder with the following content

# frozen_string_literal: true
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

Next, Go to your rails_helper.rb in the spec folder and uncomment the line

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

Next, we would stub the Twilio verify service to help us with testing. To do this, we would create a file called fake_twilio.rb file in our lib folder with the following content

class FakeTwilio
  def initialize(_account_sid, _auth_token)
  end

  def verify
    @verify ||= Verify.new
  end

end

class Verify
  def initialize
  end

  def services(sid)
    @services = Services.new
  end

end

class Services
  def initialize
  end

  def verifications
    @verifications = Verifications.new
  end

  def verification_checks
    @verification_checks = VerificationChecks.new
  end
end

class Verifications
  def initialize
  end

  def create(to:, channel:)
    return { to: to, channel: channel }
  end
end

class VerificationChecks
  def initialize
  end

  def create(to:, code:)
    return OpenStruct.new(status:  'approved')
  end
end

Next we would add the stub to our spec_helper.rb file in our rspec folder. To do that, let's use the code below

config.before(:each) do
    stub_const("Twilio::REST::Client", FakeTwilio)
  end

Next, open application.rb file in the config folder. Add the following line just under config.load_defaults 5.2.

config.eager_load_paths << Rails.root.join('lib')

Before we move folder, let add faker gem to help us autogenerate user data for our User attribute. Add this line in the development and test section.

gem 'faker'

Next, let's add a User factory. To do that, create a factory folder in the spec folder. Create a users.rb file in the factory folder with the following content.

FactoryBot.define do

    factory :user do
      name { Faker::Name.name }
      phone_number { Faker::PhoneNumber.phone_number }
      country_code { '+234' }
    end
  end
  

Next, let's create the users controller request test file. To do that, let's create a controller folder in the spec folder. In the controller folder, let's create a users_controller_spec.rb file. With the following content

require 'rails_helper'

RSpec.describe UsersController, type: :request do
  it 'should send otp code to user phone number' do
    user = build(:user)
    post '/send_otp_code', params: {user: {phone_number: user.phone_number, country_code: user.country_code}}
    payload = JSON.parse(response.body)

    expect(response.status).to eq 200
    expect(payload["message"]).to eq("An activation code would be sent to your phonenumber!")
    expect(payload["country_code"]).to eq user.country_code.to_s
    expect(payload["phone_number"]).to eq user.phone_number.to_s
  end
end

Next, run rspec.

On your terminal

rspec

This would give you an error that looks like this

Screenshot 2020-09-08 at 21.47.36.png

From the error above, we need to add a POST /send_otp_code to our route.rb file. Let's go ahead and do that. Add this line inside your route.rb file in the config folder.

post '/send_otp_code', as: 'user_send_otp_code', to: 'users#send_code'

Next, run rspec again on your terminal.

rspec

This should throw another error like this

Screenshot 2020-09-08 at 21.51.38.png
From the error above, we need to add the send_code action to our UserController. Let's go ahead and do that.

Open the controllers folder inside app folder. Open the users controller file and add the send_code action as shown below

  def send_code
    response = VerificationService.new(
      user_params[:phone_number],
      user_params[:country_code]
    ).send_otp_code

    render json: {
      phone_number: user_params[:phone_number],
      country_code: user_params['country_code'],
      message: response
    }
  end

The entire file should look like this

class UsersController < ApplicationController
  def send_code

    response = VerificationService.new(
      user_params[:phone_number],
      user_params[:country_code]
    ).send_otp_code

    render json: {
      phone_number: user_params[:phone_number],
      country_code: user_params['country_code'],
      message: response
    }
  end

private
  def user_params
    params.require(:user).permit(
      :name, :email, :country_code, :phone_number
    )
  end
end

Next, run rspec again on the terminal

rspec

We should have another error uninitialized constant UsersController::VerificationService. The terminal should look like this

Screenshot 2020-09-08 at 22.08.31.png

We are making progress. Next let's create the VerificationService class. To do that, create a folder called services inside the app folder and create a file called verification_service.rb. The content of the file should look like this

class VerificationService
  def initialize(phone_number, country_code)
    @phone_number = phone_number
    @country_code = country_code
    @phone_number_with_code = "#{@country_code}#{@phone_number}"
  end

  def send_otp_code
    SendPinWorker.perform_async(
      @phone_number_with_code
    )
    'An activation code would be sent to your phonenumber!'
  end
end

From this code we are getting this phone number and country code from params and then concatenating it to give us a complete valid phone number and we using a worker class to process the send pin code request. This prevents the SMS flow from happening on the main thread.

Next, let's run rspec again on the terminal

rspec

We should get another error on the terminal uninitialized constant VerificationService::SendPinWorker. Next we would create the SendPinWorker class. To do that let's create a folder called workers in the app folder. Next create the file, send_pin_worker.rb with following content.

class SendPinWorker
  include Sidekiq::Worker

  def perform(phone_number_with_code)
    account_sid = ENV['ACCOUNT_SID']
    auth_token = ENV['AUTH_TOKEN']
    service_sid = ENV['SERVICE_SID']

    client = Twilio::REST::Client.new(account_sid, auth_token)
    verification_service = client.verify.services(service_sid)

    verification_service
      .verifications
      .create(to: phone_number_with_code, channel: 'sms')
  end
end

From the above code snippet, we are initializing the Twilio Rest Client class with the account_sid and auth_token and then we are calling the verify services method with the service_sid. The verification service would trigger a create function to send the otp code to the provided phone number.

Next, let's run rspec again on the terminal

Hurray๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰. We should see the test passing now.

Screenshot 2020-09-08 at 22.22.24.png

Next, we would test our endpoint on Postman. To install postman on your machine. Check out this link to download Postman

Next, start the rails server, redis server and sidekiq To do that run the following on the terminal

rails s

Run this on another terminal

bundle exec sidekiq

Run this on another terminal

redis-server

Next, open postman and add the the url and the body of the request as shown in the image below

Screenshot 2020-09-08 at 22.33.20.png

Next, hit send to make the request. Check the response body for a response as shown below
Screenshot 2020-09-09 at 13.11.41.png
Next, check your Messaging app on your phone device, you should have gotten a 4 digit code. Amazing right?

Next, we are going create another method in our class to verify the 4 digit code we just received. But before we do that, remember TDD ๐Ÿ˜. Let add our test that verifies the 4 digit code and creates a new user on our database when code is verified. To do that, Open the users_controller_spec.rb file in the controller folder in our spec folder. Let add another assertion to test that the user get's created when the 4 digit code is verified.

Add the following code to your users_controller_spec.rb file

  it 'should create a new verified user' do
    user = build(:user)
    post '/users', params: {user: {phone_number: user.phone_number, country_code: user.country_code, otp_code: "1234"}}
    payload = JSON.parse(response.body)
    auth_token = payload["auth_token"]
    user_token = JsonWebToken.decode(auth_token)

    expect(response.status).to eq 201
    expect(payload["user"]["phone_number"]).to eq(user.phone_number)
    expect(payload["user"]["country_code"]).to eq(user.country_code)
    expect(user_token["user_id"]).to eq(User.first.id)
    expect(payload["message"]).to eq "Phone number verified!"
  end

Next, stop your rails server and run rspec

You should get an error on your terminal No route matches [POST] "/users" Let's add the route to our route.rb file. Open the route.rb file in the config folder and add the following line under the first route created earlier.

resources :users, only: [:create]

run rspec again on the terminal. We should get another error, The action 'create' could not be found for UsersController. Let's add the create action to our UsersController.

def create
  if verify_otp_code?
      user = User.find_or_create_by(
        country_code: user_params[:country_code],
        phone_number: user_params[:phone_number]
      )
      token = JsonWebToken.encode(user_id: user.id)
      render json: {
        user: user,
        auth_token: token,
        message: 'Phone number verified!'
      }, status: :created
    else
      render json: { data: {}, message: 'Please enter a valid phone number' },
             status: :unprocessable_entity
    end
  rescue Twilio::REST::RestError
    render json: { message: 'otp code has expired. Resend code' }, status: :unprocessable_entity
end

def verify_otp_code?
    VerificationService.new(
      user_params[:phone_number],
      user_params[:country_code]
    ).verify_otp_code?(params['otp_code'])
 end

From the code above, we are verifying the 4 digit code. If it is approved, we are creating a new user. Next, we are encoding the user id using JWT and returning the token with user information in our response. We are also rescuing any Twilio error.

run rspec again on the terminal. You should see the error, undefined method `verify_otp_code?' for #<VerificationService:0x00007f84b44d5608>. From the error, let's create the verify_otp_code method in the VerificationService class. Do that, add the following lines of code. In the initialize method of the service, add these

    @account_sid = ENV['ACCOUNT_SID']
    @auth_token = ENV['AUTH_TOKEN']
    @service_sid = ENV['SERVICE_SID']

    client = Twilio::REST::Client.new(@account_sid, @auth_token)
    @verification_service = client.verify.services(@service_sid)

Next, create the verify_otp_code method as shown below

def verify_otp_code?(otp_code)
    verification_check =  @verification_service
                          .verification_checks
                          .create(to: @phone_number_with_code, code: otp_code)

    verification_check.status == 'approved'
  end

The entire VerificationService file looks like this

class VerificationService
  def initialize(phone_number, country_code)
    @phone_number = phone_number
    @country_code = country_code
    @account_sid = ENV['ACCOUNT_SID']
    @auth_token = ENV['AUTH_TOKEN']
    @service_sid = ENV['SERVICE_SID']

    client = Twilio::REST::Client.new(@account_sid, @auth_token)
    @verification_service = client.verify.services(@service_sid)

    @phone_number_with_code = "#{@country_code}#{@phone_number}"
  end

  def send_otp_code
    SendPinWorker.perform_async(
      @phone_number_with_code
    )
    'An activation code would be sent to your phonenumber!'
  end

  def verify_otp_code?(otp_code)
    verification_check =  @verification_service
                          .verification_checks
                          .create(to: @phone_number_with_code, code: otp_code)

    verification_check.status == 'approved'
  end
end

Next, run rspec again.
You should get another error like this NameError: uninitialized constant UsersController::JsonWebToken. Let's create the JsonWebToken class. To do that, we need to install jwt gem. Add this line to your Gemfile

gem 'jwt'

Run bundle install

bundle install

Next, let's create our JsonWebToken class in our lib folder. create a file json_web_token.rb with the following content.

class JsonWebToken
  SECRET_KEY = Rails.application.secrets.secret_key_base. to_s

  # Token expires 5 years from now
  def self.encode(payload, exp = 48.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new decoded
  end
end

From the code snippet above, I am creating two method encode and decode. The encode method convert the user.id into a JSON Web Token. The decode method converts it back to a hash that contains the user_id and the expiry information. Also, the token would expiry in 48 hours.

Next, let's run rspec on the terminal again. The test should pass now. As shown in the image below.

Screenshot 2020-09-09 at 14.58.34.png

Next, start the rails server, sidekiq and redis-server all in different terminals as mentioned earlier.

Next, we would test the endpoint on Postman. To do that, launch Postman and use the send_otp_code route to get the pin code. Next use the create user route to create a new user as shown in the image below:
STEP 1
Screenshot 2020-09-09 at 15.12.11.png

STEP 2
Screenshot 2020-09-09 at 13.11.41.png

STEP 3
Screenshot 2020-09-09 at 15.23.06.png

Oops!๐Ÿ˜” an error.
Screenshot 2020-09-09 at 15.10.35.png

Let's fix this. This is because we are using phone_number as an integer but the integer value is about 10 digits which is above the range for the integer datatype. We need to bring in the big boys ๐Ÿ˜. We need the bigint data type. To change our datatype for phone_number, we need a new migration file. Run the code below to generate a new migration file

rails g migration ChangeDatatypeInPhoneNumber

Next, open the migration file inside db > migrate. Add the following line in the change method

 change_column :users, :phone_number, :bigint

The migration file should look like this

class ChangeDatatypeInPhoneNumber < ActiveRecord::Migration[5.2]
  def change
    change_column :users, :phone_number, :bigint
  end
end

Run rails db:migrate
Let's go to postman and repeat the steps(1-3) mention above. We should see our response on postman as shown below

Screenshot 2020-09-09 at 15.26.51.png

And that is it!!!. ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰. Wow! This is some lengthy article but I hope this explains the process. In the third part of this article, I would be showing you how to integrate this to our React Native Mapbox app so we can have some basic authentication flow before seeing our map. To check out the article that explains how to create our react native with Mapbox app, click on this link. See you in the next one!

Discover and read more posts from Daniel Amah
get started
post commentsBe the first to share your opinion
Show more replies