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:
- Ensure that the caller is a known participant by checking their caller ID against a list of permitted phone numbers
- 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:
- Ruby and Bundler 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)
- A Twilio phone number that can receive incoming calls
- ngrok for testing webhooks with our local application
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
Install the dependencies:
Start the server to make sure everything is working as expected:
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:
$ 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 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
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
Install the dependency then run the envyable install task:
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 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"
Next, generate a new controller for our verified conference calls:
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. 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
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.
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
welcome action we will check whether the caller is permitted to join the conference by checking against the
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
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> to deliver a message to the caller and
<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
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
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
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.
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
PERMITTED_PARTICIPANTS variables in
config/env.yml Start the application with:
bundle exec rails server
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"
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.
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
We also need a Verify Service Sid. To get one, create a new Verify Service in your Twilio console.
Grab the Verify Service Sid and add it to
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 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")
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"
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)
welcome action, replace the redirect with the call to
start_verification using the caller's number and then use
<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
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
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:
- Expand your OTP channels with email
- Verify your users' phone numbers in your Rails web application
- 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 or over email at firstname.lastname@example.org.