This is the third part of a five-part guide that shows you how to build a web app to co-ordinate a gift exchange. We’ll use Flask, a Python web framework, and Infobip’s Python SDK.

In this part of the guide, we add verification for users’ phone numbers, using Infobip’s 2-factor authentication API.

You can follow along with the code at the project repository on GitHub, where each step of this guide corresponds to a branch in the repo.

Setting up our Infobip details

This is where we add some Infobip magic into our app. To get started with this, you’ll need the following bits of information:

  • Your Infobip API key
  • Your Infobip Base URL

You can manage and create API credentials at https://portal.infobip.com/dev/api-keys after logging in to your Infobip account. Your Base URL is also visible in your portal homepage; see the documentation for more.

We’ll export these credentials as environment variables by running the following commands in our terminal:

export IB_API_KEY=<your API key here>
export IB_BASE_URL=<your base URL here>

Next, we’ll set up our 2FA application within Infobip. This is where you configure things like the format of the verification PINs, how long they remain valid, and how many retries are allowed per number per day.

To do this, create a new script in your project folder called set_up_2fa_application.py and add the following code into it:

from infobip_channels.sms.channel import SMSChannel

channel = SMSChannel.from_env()

response = channel.create_tfa_application(
    {
    "name": "gift exchange 2fa application",
    "enabled": "true",
    "configuration": {
        "pinAttempts": 7,
        "allowMultiplePinVerifications": "true",
        "pinTimeToLive": "11m",
        "verifyPinLimit": "2/4s",
        "sendPinPerApplicationLimit": "5000/12h",
        "sendPinPerPhoneNumberLimit": "2/1d",}
    }

)

response = channel.get_tfa_applications()
application_id = response.list[0].application_id
print(application_id)

response = channel.create_tfa_message_template(
    application_id,
    {
            "pinType": "NUMERIC",
            "pinPlaceholder": "{{pin}}",
            "messageText": "Your pin is {{pin}}",
            "pinLength": 4,
            "language": "en",
            "senderId": "Infobip 2FA",
            "repeatDTMF": "1#",
            "speechRate": 1,
    }
)
template_id = response.list[0].message_id
print(template_id)

There are two calls to the Infobip API here, with the first being the creation of our 2FA application, and the second being the creation of a message template within that application. To learn more about this, and what each part of the configuration is doing, check out the API documentation.

The other things that this script does is print out two IDs that we’ll need to use later. The first is the Application ID, and the second is the Message Template ID.

When you run this script, the Infobip Python SDK uses the credentials that you exported into your environment variables to create a Channel instance, with the SMSChannel.from_env() method.

Run this script from your terminal with the following command:

python set_up_2fa_application.py

This will print the two IDs that you need to your terminal output, like this:

Application ID: <Your Application ID>
Template ID: <Your Template ID>


We’ll export these to our environment variables as well:

export TFA_APP_ID=<the application ID output by the script>
export TFA_TEMPLATE_ID=<the template ID output by the script>

To use these in our app, we’ll need to edit our app.py file. Add the following lines of code somewhere before your index() method:

tfa_application_id = os.environ['TFA_APP_ID']
tfa_template_id = os.environ['TFA_TEMPLATE_ID']

Now our app is able to use that 2FA application and and its message template.

Sending a verification PIN over text

Next, we need to use this Infobip application to send our users a verification PIN when they add their number to our app.

We’ll write a new function in our app.py file. Add this code:

def send_verification_text(number):
    channel = SMSChannel.from_env()
    response = channel.send_pin_over_sms(
        {"ncNeeded": "false"},
        {
            "applicationId": tfa_application_id,
            "messageId": tfa_template_id,
            "from": "InfoSMS",
            "to": number,
        }
    )
    save_pin_id_for_number(number, response.pin_id)

This function takes a phone number and sends a PIN to it. Infobip keeps track of verification PINs by giving them each an ID, which is stored on the Infobip end. When we want to verify that a PIN is valid, we’ll need that ID, which we get from the API’s response. So, we need to store that PIN ID somewhere in our app. This is what the function save_pin_id_for_number does, and here’s the code for that function:

def save_pin_id_for_number(number, pin_id):
    conn = sqlite3.connect('database.db')
    cur = conn.cursor()
    cur.execute(f"""
        UPDATE people
        SET verification_code = '{pin_id}'
        WHERE phone_number = {number};"""
    )
    conn.commit()
    conn.close()

Here, we’re using SQLite3 again to store the verification PIN ID (not the same thing as the PIN itself, we’re not storing the PIN) alongside the data for each person. We do this by updating only the row where the phone number is the one which has just received a text.

Lastly, we need to actually call the send_verification_text function from our main index function. Update that code to look like this:

@app.route('/', methods=['GET', 'POST'])
def index():
   if request.method == 'GET':
       pass
   else:
       name = request.form.get('name')
       number = request.form.get('number')
       if not all((name, number)):
           flash("Please enter both a name and a phone number.")
       else:
           print(f"You clicked the button with name {name} and number {number}!")
           add_new_person_to_db(name, number)
           send_verification_text(number)
           flash(f"You added {name} with number {number}!")
   return render_template('app.html')

Now, when a user adds their name and number into our app, they’ll also get a text to their phone with a PIN code.

Adding a page for users to verify their PIN

Now, we need to build a way for users to verify that their PIN code is valid. To do this, we’ll add a /verify page to our app. First, let’s add some HTML; create a verify.html file within your app’s templates/ folder, and add this code to it:

<html>
    <head>
        <title>Gift Exchange App</title>
        <link rel="icon" type="image/favicon" href="https://avatars.githubusercontent.com/u/1288491">
        <link href="https://fonts.googleapis.com/css2?family=Lora:wght@600&display=swap" rel="stylesheet">
    </head>
    <body>
        {% with msg = get_flashed_messages() %}
            {% if msg %}
                {% for message in msg %}
                    <strong>{{ message }}</strong>
                {% endfor %}
            {% endif %}
        {% endwith %}
        <form action="{{ url_for('verify')}}" method="POST">
            <p><input type="text" name="name" placeholder="Enter your name here"></p>
            <p><input type="tel" name="number" placeholder="Enter your phone number here"></p>
            <p><input type="number" name="pin" placeholder="Enter your PIN code here"></p>
            <p><button type="submit"><p>Verify my number!</p></button></p>
        </form>
    </body>
 </html>

This is very similar to the code for index.html, with the main differences being that the form has one more field – for the PIN code – and sends its data to the verify page rather than the main index page. That means we need to add code to our app to handle requests coming in to the /verify endpoint.

In your app.py file, add the following code:

@app.route('/verify', methods=['GET', 'POST'])
def verify():
   if request.method == 'GET':
       pass
   else:
       name = request.form.get('name')
       number = request.form.get('number')
       pin = request.form.get('pin')
       if not all((name, number, pin)):
           flash("Please enter all required details.")
       else:
           print(f"You clicked the button with name {name}, number {number} and PIN {pin}!")
           verify_number(name, number, pin)
   return render_template('verify.html')

This code is, again, fairly similar to what our index code used to look like a few steps ago. The main difference here is that we’re taking three pieces of data – name, number and PIN – and instead of saving them to a database, we’re calling a verify_number function with them.

Here’s what the verify_number code looks like:

def verify_number(name, number, pin):
    conn = sqlite3.connect('database.db')
    cur = conn.cursor()
    cur.execute(f"""SELECT verification_code from people
                WHERE name = '{name}' AND phone_number = {number}""")
    pin_id = cur.fetchone()[0]
    channel = SMSChannel.from_env()
    response = channel.verify_phone_number(
        pin_id,
        {"pin": f"{pin}"}
    )
    if response.verified:
        cur.execute(f"""
            UPDATE people
            SET verified = 1
            WHERE phone_number = {number};"""
        )
        conn.commit()
        flash(f"Your number has been verified!")
    else:
        flash(f"Error: Number not verified: {response.pin_error}")
    conn.close()

This looks like a lot! Firstly, the function takes the name and number of the person who submitted the PIN, and uses that to retrieve their data from our database. Specifically, we’re retrieving the verification ID that we got when the text was originally sent.

We then use the Pin ID to query the Infobip API, and see if the PIN that the user submitted matches what Infobip has on file for that verification ID.

Finally, we check the response from Infobip to see whether the PIN was correct or not, with response.verified. If the PIN was correct, we update the database to reflect that that user’s number is verified; if not, we send an error message to the user.

And that’s it! We’ve built a verification flow for our users, and we’re ready for them to start buying gifts for each other. In the next step, we’ll build the part of the app that tells our users who they should buy a gift for by sending them texts.