How to build a one-time passcode protected conference line with Twilio Verify and Ruby
Tags: ruby, rails, twilio, otp
Published: 2020-08-12 06:05:41

---
title: How to build a one-time passcode protected conference line with Twilio Verify and Ruby
published: true
description: 
tags: ruby, rails, twilio, otp
cover_image: https://twilio-cms-prod.s3.amazonaws.com/images/wLz4bTldfwtI5b4AvhKomAwRm2yKBY-Eg4sUflaPhpkPM.width-1616.png
canonical_url: https://www.twilio.com/blog/one-time-passcode-protected-conference-line-twilio-verify-ruby
series: Conference Calls on Rails
---

We've seen [how to build a conference line](https://www.twilio.com/blog/build-conference-line-twilio-ruby) and then [protect it with a static passcode](https://www.twilio.com/blog/passcode-protected-conference-line-twilio-ruby). 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](https://en.wikipedia.org/wiki/Caller_ID_spoofing) 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](https://github.com/philnash/conference-calls-on-rails) 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](https://www.twilio.com/docs/verify/api) 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:

*   [Ruby](https://www.ruby-lang.org/en/downloads/) and [Bundler](https://bundler.io/) installed
*   A Twilio account (if you don't have one yet, [sign up for a new Twilio account here and receive $10 credit when you upgrade](https://twil.io/philnash))
*   A [Twilio phone number](https://www.twilio.com/user/account/phone-numbers/incoming) that can receive incoming calls
*   [ngrok](https://ngrok.com/) for [testing webhooks with our local application](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html)

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](https://github.com/philnash/conference-calls-on-rails).

```bash
git clone https://github.com/philnash/conference-calls-on-rails.git -b passcode-protected-conference
cd conference-calls-on-rails
```

Install the dependencies:

```bash
bundle install
```

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

```bash
bundle exec rails server
```

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:

```bash
$ 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>
```

This response is [in TwiML](https://www.twilio.com/docs/voice/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](https://www.twilio.com/blog/passcode-protected-conference-line-twilio-ruby). 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](https://www.twilio.com/blog/2015/02/managing-development-environment-variables-across-multiple-ruby-applications.html). Open the `Gemfile` and add the gem to the development and test group:

```ruby
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
```

Install the dependency then run the [envyable install task](https://github.com/philnash/envyable#install-task):

```bash
bundle install 
bundle exec envyable install
```

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](https://www.twilio.com/docs/glossary/what-e164) 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

```yaml
PERMITTED_CALLERS: "+61422222222,+61433333333"
MODERATOR: "+61411111111"
```

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

```bash
bundle exec rails generate controller verified_calls
```

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](https://guides.rubyonrails.org/security.html#csrf-countermeasures). 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](https://www.twilio.com/blog/2014/09/securing-your-ruby-webhooks-with-rack-middleware.html) instead.

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

```ruby
class VerifiedCallsController < ApplicationController
  skip_before_action :verify_authenticity_token
end
```

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](https://www.twilio.com/docs/libraries/ruby) to generate the [TwiML response](https://www.twilio.com/docs/voice/twiml) and render it as XML.

```ruby
class VerifiedCallsController < ApplicationController
  skip_before_action :verify_authenticity_token

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

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.

```ruby
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
```

Use the `participant_permitted?` method to check that the caller's number, [the `From` parameter in the request](https://www.twilio.com/docs/voice/twiml#request-parameters), 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.

```ruby
  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
```

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.

```ruby
  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
```

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

```ruby
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
```

### 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:

```bash
bundle exec rails server
```

The application will start up on localhost:3000\. Next, start [ngrok](https://ngrok.com) so that we can get a public URL for this application to use with Twilio's webhooks.

```bash
ngrok http 3000 --host-header "localhost:3000"
```

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](https://www.twilio.com/console/phone-numbers/incoming) 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.](https://twilio-cms-prod.s3.amazonaws.com/images/rkd6D5swZENptiYY1DUUKUCPuTnnjNg3q9CmW5kAXhu5_N.width-500.png)

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](https://www.twilio.com/docs/verify/api) 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](https://www.twilio.com/console). Add them to `config/env.yml` like so:

```yaml
PERMITTED_CALLERS: "+61422222222,+61433333333"
MODERATOR: "+61411111111"
TWILIO_ACCOUNT_SID: YOUR_ACCOUNT_SD
TWILIO_AUTH_TOKEN: YOUR_AUTH_TOKEN
```

We also need a Verify Service Sid. To get one, create a new [Verify Service in your Twilio console](https://www.twilio.com/console/verify/services).

![The general settings page for your Verify Service. The Service Sid will be displayed under the service name.](https://twilio-cms-prod.s3.amazonaws.com/images/ZzYNpf0-CE3vNpPi31eMs91XunbwVkkaLgHTYATnzY6pMP.width-500.png)

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

```yaml
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
```

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](https://www.twilio.com/docs/verify/api/verification) and ask them to enter the passcode.

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

```ruby
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")
```

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](https://www.twilio.com/docs/verify/api/verification-check) 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:

```ruby
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"
```

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:

```ruby
  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)
```

In the `welcome` action, replace the redirect with the call to `start_verification` using the caller's number and then use [`<Gather>`](https://www.twilio.com/docs/voice/twiml/gather) and [`<Say>`](https://www.twilio.com/docs/voice/twiml/say) to request the user input the code.

```ruby
  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
```

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.

```ruby
  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
```

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](https://www.twilio.com/blog/build-conference-line-twilio-ruby), [how to protect that conference line with a passcode](https://www.twilio.com/blog/passcode-protected-conference-line-twilio-ruby) 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](https://github.com/philnash/conference-calls-on-rails).

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:

*   [Expand your OTP channels with email](https://www.twilio.com/docs/verify/email)
*   [Verify your users' phone numbers in your Rails web application](https://www.twilio.com/docs/verify/quickstarts/ruby-rails)
*   [Sanitize phone numbers before sending mass alerts](https://www.twilio.com/blog/sanitize-phone-numbers-before-sending-mass-alerts)

Any further questions? I'd be glad to help, just drop me a note on [Twitter at @philnash](https://twitter.com/philnash) or over email at [philnash@twilio.com](mailto:philnash@twilio.com).