How to build a one-time passcode protected conference line with Twilio Verify and Ruby

philnash

Phil Nash

Posted on August 12, 2020

How to build a one-time passcode protected conference line with Twilio Verify and Ruby

We've seen how to build a conference line and then protect it with a static passcode. However, passcodes can be guessed or leaked, especially if they are reused over time. An alternative is to make a list of numbers that are permitted to join the call. But, since spoofing phone numbers is relatively easy, this still may not protect you.

A one-time passcode (OTP) sent to a caller's phone or email, can verify they are who they say they are and increase the security of your conference line once more.

In this post we will take the Rails application we previously developed and add a conference line secured in two ways. We will:

  1. Ensure that the caller is a known participant by checking their caller ID against a list of permitted phone numbers
  2. Send them an OTP using Twilio Verify which they then have to enter correctly to ensure they aren't spoofing the number

What you'll need

In order to code along with this tutorial you will need:

Once you've got all that, we can get started.

On the Rails

We will use the existing Rails application that we've added a couple of conference lines to so far. We'll continue to add to that app in this post. If you don't already have the application, download or clone it from GitHub.

git clone https://github.com/philnash/conference-calls-on-rails.git -b passcode-protected-conference
cd conference-calls-on-rails
Enter fullscreen mode Exit fullscreen mode

Install the dependencies:

bundle install
Enter fullscreen mode Exit fullscreen mode

Start the server to make sure everything is working as expected:

bundle exec rails server
Enter fullscreen mode Exit fullscreen mode

While the server is running, you can make a POST request to the open conference call webhook endpoint to see it working. It should look a bit like this:

$ curl --data "" localhost:3000/calls
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="alice">Welcome to the conference call, let's dial you in right away.</Say>
  <Dial>
    <Conference>Thunderdome</Conference>
  </Dial>
</Response>
Enter fullscreen mode Exit fullscreen mode

This response is in TwiML and tells Twilio what to do with a call. If we set a phone number's incoming call webhook to the /calls endpoint within this application, then anyone who calls will be welcomed and then entered into the conference call.

In the last blog post we added an endpoint that would enforce passcode protection on your conference call. This version is available at the /protected_calls/welcome and /protected_calls/verify routes.

Let's get started building verified called protection.

Protecting a call with a list of permitted callers

We're going to set the list of permitted callers using environment variables. To make that easier to manage we'll use the envyable gem. Open the Gemfile and add the gem to the development and test group:

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 "envyable", "~> 1.2"
end
Enter fullscreen mode Exit fullscreen mode

Install the dependency then run the envyable install task:

bundle install 
bundle exec envyable install
Enter fullscreen mode Exit fullscreen mode

The install task creates a config/env.yml file, open that up and fill in two variables:

  • PERMITTED_CALLERS: a comma separated list of numbers in e.164 format that will be allowed into your conference call
  • MODERATOR: the number of your conference call moderator, this will be the caller that starts and ends the conference call
PERMITTED_CALLERS: "+61422222222,+61433333333"
MODERATOR: "+61411111111"
Enter fullscreen mode Exit fullscreen mode

Next, generate a new controller for our verified conference calls:

bundle exec rails generate controller verified_calls
Enter fullscreen mode Exit fullscreen mode

Open your new controller located in app/controllers/verified_calls_controller.rb. The first thing we need to do in this controller is disable cross site request forgery (CSRF) protection. This controller will be used to respond to incoming webhooks, which cannot include a CSRF token. We can protect webhook endpoints in our Rails application by validating Twilio's request signature using Rack middleware instead.

To disable the CSRF protection, add the following line to the controller class:

class VerifiedCallsController < ApplicationController
  skip_before_action :verify_authenticity_token
end
Enter fullscreen mode Exit fullscreen mode

We're going to add two actions to this controller, one that will check whether the caller is on our permitted list of callers and the other to enter them into the conference. This could be done within one action, but when we add OTP verification later we will need two actions.

Create the welcome action. We'll use the helpers from the twilio-ruby library to generate the TwiML response and render it as XML.

class VerifiedCallsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def welcome
    twiml = Twilio::TwiML::VoiceResponse.new
    render :xml
  end
end
Enter fullscreen mode Exit fullscreen mode

In the welcome action we will check whether the caller is permitted to join the conference by checking against the PERMITTED_CALLERS and MODERATOR environment variables. We can write a private method to make what it is doing more obvious.

class VerifiedCallsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def welcome
    twiml = Twilio::TwiML::VoiceResponse.new
    render :xml
  end

  private

  def participant_permitted?(from)
    is_moderator?(from) || ENV["PERMITTED_CALLERS"].split(",").include?(from)
  end

  def is_moderator?(from)
    from == ENV["MODERATOR"]
  end
end
Enter fullscreen mode Exit fullscreen mode

Use the participant_permitted? method to check that the caller's number, the From parameter in the request, is allowed and then redirect to the next path. If the number isn't recognised, we will use [<Say>](https://www.twilio.com/docs/voice/twiml/say) to deliver a message to the caller and [<Hangup>](https://www.twilio.com/docs/voice/twiml/hangup) to end the call.

  def welcome
    twiml = Twilio::TwiML::VoiceResponse.new
    if participant_permitted?(params["From"])
      twiml.redirect(verified_calls_verify_path)
    else
      twiml.say(voice: "alice", message: "Sorry, I don't recognize the number you're calling from.")
      twiml.hangup
    end
    render xml: twiml
  end
Enter fullscreen mode Exit fullscreen mode

We will use the path verified_calls_verify_path which we are yet to define. We will need the verify action for that. For now, this action won't verify anything, it will just add the caller to the conference. We'll use some of the <Conference> TwiML attributes available here to control the call more effectively. The boolean attributes start_conference_on_enter and end_conference_on_exit are useful for moderated conferences. In this case, all participants will be held listening to hold music until the moderator dials in and joins the conference. The call will also be terminated once the moderator hangs up. Place this method after the welcome method.

  def verify
    twiml = Twilio::TwiML::VoiceResponse.new
    twiml.say(voice: "alice", message: "Thank you, joining the conference now.")
    twiml.dial do |dial|
      dial.conference(
        "Verified",
        start_conference_on_enter: is_moderator?(params["From"]),
        end_conference_on_exit: is_moderator?(params["From"]),
      )
    end
    render xml: twiml
  end
Enter fullscreen mode Exit fullscreen mode

Let's define the routes for these actions now. Open config/routes.rb and add POST routes for both actions in this controller.

Rails.application.routes.draw do
  resources :calls, only: [:create]
  post 'protected_calls/welcome', to: 'protected_calls#welcome'
  post 'protected_calls/verify', to: 'protected_calls#verify'
  post 'verified_calls/welcome', to: 'verified_calls#welcome'
  post 'verified_calls/verify', to: 'verified_calls#verify'
end
Enter fullscreen mode Exit fullscreen mode

Testing out the line so far

Let's hook this application up to a phone number and test that it's working. Then we can add OTP verification to make the line extra secure.

Make sure that your own phone number is set as either in either the MODERATOR or PERMITTED_PARTICIPANTS variables in config/env.yml Start the application with:

bundle exec rails server
Enter fullscreen mode Exit fullscreen mode

The application will start up on localhost:3000. Next, start ngrok so that we can get a public URL for this application to use with Twilio's webhooks.

ngrok http 3000 --host-header "localhost:3000"
Enter fullscreen mode Exit fullscreen mode

You will get a URL like https://RANDOM_SUBDOMAIN.ngrok.io, put it together with the path to the welcome action we just wrote to make https://RANDOM_SUBDOMAIN.ngrok.io/verified_calls/welcome. Open the Twilio console and edit the phone number you chose to use for this application. Add the URL as the voice webhook for when calls come in and save the number.

When you edit your phone number in the Twilio console, enter your webhook URL in the Voice section for when a call comes in.

Now call your number; you will be greeted and allowed to enter the conference call.

Remove your number from the permitted participants and call up again. This time you will be rejected from the conference call.

Securing the conference call with an OTP

Now we're going to make sure that callers are definitely who they say they are by using Twilio Verify to send them an OTP that they will have to enter.

For this feature, we are going to need to store some more details in the environment. We're going to make requests to the API, so we'll need the Account Sid and Auth Token from your Twilio console. Add them to config/env.yml like so:

PERMITTED_CALLERS: "+61422222222,+61433333333"
MODERATOR: "+61411111111"
TWILIO_ACCOUNT_SID: YOUR_ACCOUNT_SD
TWILIO_AUTH_TOKEN: YOUR_AUTH_TOKEN
Enter fullscreen mode Exit fullscreen mode

We also need a Verify Service Sid. To get one, create a new Verify Service in your Twilio console.

The general settings page for your Verify Service. The Service Sid will be displayed under the service name.

Grab the Verify Service Sid and add it to config/env.yml too.

PERMITTED_CALLERS: "+61422222222,+61433333333"
MODERATOR: "+61411111111"
TWILIO_ACCOUNT_SID: YOUR_ACCOUNT_SD
TWILIO_AUTH_TOKEN: YOUR_AUTH_TOKEN
VERIFY_SERVICE_SID: YOUR_VERIFY_SERVICE_SID
Enter fullscreen mode Exit fullscreen mode

Head back to app/controllers/verified_calls_controller.rb. The plan now is for the welcome action to check whether the caller is permitted to join the conference and, if they are, start a verification and ask them to enter the passcode.

Starting an SMS verification with the Twilio Ruby library looks like this:

client = Twilio::REST::Client.new(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
service = client.verify.services(ENV["VERIFY_SERVICE_SID"])
service.verifications.create(to: NUMBER, channel: "sms")
Enter fullscreen mode Exit fullscreen mode

When they enter the code Twilio will make a request to the verify action, passing the digits of the code as the Digits parameter. We'll then use the code to check the verification and if it is approved send the caller into the conference call. If the code is incorrect we'll direct them back to the welcome action and ask them to enter the code again.

Checking a verification looks like this:

client = Twilio::REST::Client.new(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
service = client.verify.services(ENV["VERIFY_SERVICE_SID"])
check = service.verification_checks.create(to: NUMBER, code: CODE)
check.status == "approved"
Enter fullscreen mode Exit fullscreen mode

Let's start by adding some more private helper methods: one to authorise an API client with your Account Sid and Auth Token and return the Verify service using the Verify Service Sid, and one to use the Verify service to start the verification for the caller and one to check the result:

  private

  def verify_service
    client = Twilio::REST::Client.new(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
    client.verify.services(ENV["VERIFY_SERVICE_SID"])
  end

  def start_verification(number)
    verify_service.verifications.create(to: number, channel: "sms")
  end

  def verification_approved?(number, code)
    verify_service.verification_checks.create(to: number, code: code).status == "approved"
  end

  def participant_permitted?(from)
Enter fullscreen mode Exit fullscreen mode

In the welcome action, replace the redirect with the call to start_verification using the caller's number and then use <Gather> and <Say> to request the user input the code.

  def welcome
    twiml = Twilio::TwiML::VoiceResponse.new
    if participant_permitted?(params["From"])
      start_verification(params["From"])
      twiml.gather(action: verified_calls_verify_path) do |gather|
        gather.say(voice: "alice", message: "Please enter the code to enter the conference followed by the hash.")
      end
    else
      twiml.say(voice: "alice", message: "Sorry, I don't recognize the number you're calling from.")
      twiml.hangup
    end
    render :xml
  end
Enter fullscreen mode Exit fullscreen mode

When the caller enters the verification code and presses the hash, Twilio will send the request to the verify action. In the verify action we gate the conference entrance with the verification check. If the check succeeds we enter the conference as before and if it fails we redirect back to the welcome action and start again.

  def verify
    twiml = Twilio::TwiML::VoiceResponse.new
    if verification_approved?(params["From"], params["Digits"])
      twiml.say(voice: "alice", message: "Thank you, joining the conference now.")
      twiml.dial do |dial|
        dial.conference(
          "Verified",
          start_conference_on_enter: is_moderator?(params["From"]),
          end_conference_on_exit: is_moderator?(params["From"]),
        )
      end
   else
      twiml.say(voice: "alice", message: "Sorry, that code is incorrect.")
      twiml.redirect(verified_calls_welcome_path)
    end
    render :xml
  end
Enter fullscreen mode Exit fullscreen mode

Make sure you are in the permitted callers list again and restart the application. Call it up and you will receive an SMS with a verification code. If you enter the code incorrectly you will be asked for it again. Enter the code correctly and you will be entered into the conference, safe in the knowledge that you are indeed who you said you were.

What's next after protecting a conference line with an OTP?

Over these blog posts you've seen how to build an open conference line in Rails, how to protect that conference line with a passcode and now how to protect it with a list of permitted callers and Twilio Verify. The code for the application, which has all 3 types of conference line, can be found on GitHub.

To go further, you could expand this application with a database and save different conferences with different lists of permitted callers and moderators.

Twilio Verify can be used to protect more than conference calls with an SMS verification code. Check out how you can:

Any further questions? I'd be glad to help, just drop me a note on Twitter at @philnash or over email at philnash@twilio.com.

💖 💪 🙅 🚩
philnash
Phil Nash

Posted on August 12, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related