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

April 20, 2020
Written by

Header protected conference line

You can protect your conference call with a static passcode, and while that offers more security than nothing at all, passcodes can be guessed or leaked -- especially if they're reused over time. You can also verify the caller ID of the person calling in, but spoofing phone numbers is still easy and prevalent.

One time passcodes (OTP) offer additional security by ensuring that a user has access to the phone and number they claim to own. By sending an OTP to the user's number or email you can have confidence the person joining your call is who they say they are.

The code in this post will secure your conference line in two ways:

  1. Check that the person calling is a known participant
  2. Prevent anyone from spoofing a phone number in order to join the call with an OTP

Follow the tutorial below or check out the completed code on my GitHub.

Prerequisites for setting up protected conference calls in Python

Here’s a handy step-by-step guide to setting up Python, pip, virtualenv and Flask.

Create a basic conference call

Create a new folder to store your code files, I called mine otp-conference. Run the following commands in your terminal to set up a virtual environment and install our dependencies:

virtualenv venv
source venv/bin/activate
pip install flask==1.1.2 twilio==6.38.0

Create a settings.py file to store our credentials and update the values with your account credentials and Verify Service SID:

# find here: twilio.com/console
TWILIO_ACCOUNT_SID = 'ACxxx'
TWILIO_AUTH_TOKEN = 'xxx'

# create here: twilio.com/console/verify/services
VERIFY_SERVICE_SID = 'VAxxxx'

# replace with your number for testing
MODERATOR = '+18005551234'

# add your number and the numbers of other people joining your call
# could replace with your customer DB in production
KNOWN_PARTICIPANTS = {
  '+18005559876': 'Blathers',
  '+18005554321': 'Mabel',
  '+18005556789': 'Tommy',
  MODERATOR: 'Your name here'
}

Next, create a file called app.py and add the following code. This will ask the caller to input a 6 digit code and then connect them to a conference call -- but it doesn't actually verify those 6 digits yet.

from flask import Flask, request
from twilio.twiml.voice_response import VoiceResponse, Dial, Gather
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException


app = Flask(__name__)
app.config.from_object('settings')

client = Client(
   app.config['TWILIO_ACCOUNT_SID'],
   app.config['TWILIO_AUTH_TOKEN'])

VERIFY_SERVICE_SID = app.config['VERIFY_SERVICE_SID']
MODERATOR = app.config['MODERATOR']


def join_conference(caller, resp):
   with Dial() as dial:
       # If the caller is our MODERATOR, then start the conference when they
       # join and end the conference when they leave
       if request.values.get('From') == MODERATOR:
           dial.conference(
               'My conference',
               start_conference_on_enter=True,
               end_conference_on_exit=True)
       else:
           # Otherwise have the caller join as a regular participant
           dial.conference('My conference', start_conference_on_enter=False)
      
       resp.append(dial)
       return str(resp)


@app.route("/voice", methods=['GET', 'POST'])
def voice():
   """Respond to incoming phone calls with a menu of options"""
   # Start our TwiML response
   resp = VoiceResponse()
   caller = request.values.get('From')

   # verify the phone number has access to the call
   name = app.config['KNOWN_PARTICIPANTS'].get(caller)
   if name is None:
       resp.say("Sorry, I don't recognize the number you're calling from.")
       return str(resp)
  
   # TODO - START VERIFICATION

   # Start our <Gather> verb
   gather = Gather(num_digits=6, action='/gather')
   gather.say(
       "Welcome {}. Please enter the 6 digit code sent to your device.".format(
           name))
   resp.append(gather)

   # If the user doesn't select an option, redirect them into a loop
   resp.redirect('/voice')

   return str(resp)


@app.route('/gather', methods=['GET', 'POST'])
def gather():
   """Processes results from the <Gather> prompt in /voice"""
   # Start our TwiML response
   resp = VoiceResponse()

   # If Twilio's request to our app included already gathered digits,
   # process them
   if 'Digits' in request.values:
       # Get the one-time password input
       caller = request.values['From']

       # TODO - CHECK VERIFiCATION

       return join_conference(caller, resp)

   return str(resp)

Start your flask application by running the following command in your terminal:

flask run

You should see the server start on port 5000

...
* Running on http://127.0.0.1:5000/
...

Connect your Python code to your Twilio phone number by running the following command in a new terminal window. This will automatically create an encrypted tunnel using ngrok so Twilio can talk to the code running on your local machine. Make sure to update the number in the command to the number you purchased.

twilio phone-numbers:update +11111111111 --voice-url="http://localhost:5000/voice"

Test your unprotected conference line

Call your conference number—make sure you updated the MODERATOR number in the settings.py file to be your own—you should hear a greeting ask you for a 6 digit code. You can enter anything right now, we're not testing the passcode yet. After you enter the code you should hear some hold music.

Protect your conference call with a one time passcode (OTP)

This is already useful - conference lines are a great way to connect with customers, colleagues and friends in this time of social distancing. You may want additional safeguards to make sure you're talking to the right person.

In app.py, create a new function called start_verification:

def start_verification(caller):
   # don't send another code if there's already a pending verification
   try:
       client.verify \
           .services(VERIFY_SERVICE_SID) \
           .verifications(caller) \
           .fetch()
   except TwilioRestException as e:
       print("No pending verifications for {}".format(caller))

       # verify the user is not spoofing a phone number
       # alternative: could look up a participant's email
       # and send a code via email: twilio.com/docs/verify/email
       client.verify \
           .services(VERIFY_SERVICE_SID) \
           .verifications \
           .create(to=caller, channel='sms')

This checks to make sure the participant is on your list of known participants. Then it verifies that the caller has access to the phone number they're calling from by sending a one time passcode with the Verify API. This helps guard against phone number spoofing.

Alternatively, you could look up the caller in your database and send a verification token to the email on file. You could also ask the user to input their account ID or another identifier to look up their phone number that way.

Add another function called check_verification:

def check_verification(caller, otp):
    check = client.verify \
        .services(VERIFY_SERVICE_SID) \
        .verification_checks \
        .create(to=caller, code=otp)
  
    return check.status == 'approved'

This uses the Verify API to make sure the one time passcode is correct.

Now replace the # TODO - START VERIFICATION with the following code to call our start_verification function:

- # TODO - START VERIFICATION
+ start_verification(caller)

Head down to the /gather route and replace the # TODO - CHECK VERIFICATION and return join_conference(caller, resp) lines with the following code to call our check_verification function:

- # TODO - CHECK VERIFICATION
+ if check_verification(caller, request.values['Digits']):
+    resp.say("That was correct. Joining conference.")
    return join_conference(caller, resp)
+ else:
+    resp.say("Please try again.")
+    resp.redirect('/voice')

Restart your Python app with FLASK RUN (or if you're running Flask in debug mode it will reload automatically), make sure the CLI command is still running in the background, and try calling your conference line again. You should get a text with an OTP.

What's next after building an OTP protected conference line?

You can use Verify to protect more than conference calls. Protect logins, payments, and more with the easy to use and integrate API. Here are a few other ideas to protect and secure the services you're building in Python:

Sounds like a lot? Maybe a static passcode is what you need for now. Security is all about balancing usability and friction. Static passcodes can be a good starting point.

This tutorial builds on two other tutorials that live in the Twilio docs, check these out for more information on working with Conference calls and Gather-ing input:

Check out the completed code on GitHub. Questions? Find me on Twitter @kelleyrobinson. I can't wait to see what you protect!