Build a Site with Flask and Twilio Verify for Users to Upload a File

February 18, 2021
Written by
Diane Phan
Twilion
Reviewed by

header - Build a Flask Site that Allows Authenticated Users to Upload a File

When it comes to building a website that allows users to upload files and provide their own input, you need to consider what is necessary to protect not only your users, but your project as well.

This application incorporates Twilio Verify to generate one-time passcodes for your user to verify their identity and access your app. Verify provides an easy to use form of authentication with passcodes delivered to the user's mobile phone. For even more security, consider implementing two-factor authentication.

After authenticating the users, you can give them the option to upload an image file through your site and store the files in your project directory.

In this article, you will learn how to develop a functional website to authenticate your users and protect their identity before allowing them to upload an image file to your directory.

Tutorial requirements

Set up the environment

Create a project directory in your terminal called flaskupload to follow along. If you are using a Mac or Unix computer enter these commands in your terminal:

$ mkdir flaskupload
$ cd flaskupload
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install flask twilio python-dotenv

If you are on a Windows machine, enter the following commands in a prompt window:

text
$ md flaskupload
$ cd flaskupload
$ python -m venv venv
$ venv\bin\activate
(venv) $ pip install flask twilio python-dotenv

NOTE: Depending on what distribution of Python you are on, you might have to specify python3.

If you are curious to learn more about the packages, you can check them out here:

  • The Flask framework, to create the web application that will receive message notifications from Twilio.
  • The python-twilio package, to send messages through the Twilio service.
  • The python-dotenv package, to read a configuration file.

Create your first Twilio Verify

In order to use Twilio Verify, a service must be generated. Head to the Twilio Verify Dashboard - you should be on a page that says Services.

Click on the red plus (+) button to create a new service. Give the service a friendly name of "site-verify". The friendly name will actually show up on the text message that is sent to people's phones so if you have another specific name you would like to use, such as "<YOUR_NAME> website verify" feel free to do so.

 Click on the red Create button to confirm.

Create new service with a friendly name of site-verify on the Twilio Console

Creating the Twilio Verify service will lead you to the General Settings page where you can see the properties associated with your new Twilio Verify service.

The  general settings page of the Twilio Verify service named site-verify on the Twilio Console

Open your favorite code editor and create an .env file. Inside this file, create a new environment variable called VERIFY_SERVICE_SID. Copy and paste the SERVICE SID on the web page as the value for this new variable. .

To complete the .env file, create two additional environment variables:  TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN. You can find the values for these variables on the Twilio Console as seen below:

screenshot of the Account SID and Auth Token on the Twilio Console

Set up a development Flask server

Make sure that you are currently in the virtual environment of your project’s directory in the terminal or command prompt. Since we will be utilizing Flask throughout the project, we will need to set up the development server. Add a .flaskenv file (make sure you have the leading dot) to your project with the following lines:

FLASK_APP=app.py
FLASK_ENV=development

These incredibly helpful lines will save you time when it comes to testing and debugging        your project.

  • FLASK_APP tells the Flask framework where our application is located.
  • FLASK_ENV configures Flask to run in debug mode.

These lines are convenient because every time you save the source file, the server will reload and reflect the changes.

Then, run the command flask run in your terminal to start the Flask framework.

screenshot of the Flask environment running on development in the command line

The screenshot above displays what your console will look like after running the command flask run. The service is running privately on your computer’s port 5000 and will wait for incoming connections there. You will also notice that debugging mode is active. When in this mode, the Flask server will automatically restart to incorporate any further changes you make to the source code.

However, since you don't have an app.py file yet, nothing will happen. Though, this is a great indicator that everything is installed properly.

Feel free to have Flask running in the background as you explore the code. We will be testing the entire project at the end.

Create a database file of eligible users

For the purposes of this tutorial, we will be hardcoding a list of accounts that are allowed to enter the website, along with their phone numbers. In a production setting, you would have to use your chosen database instead.

We will be skipping passwords altogether solely for the scope of this project tutorial, however handling passwords is essential in a production setting. Keep in mind that if you were to use your own database, you would have to avoid storing passwords as plaintext.

There are plenty of libraries that help developers manage passwords in a Flask application such as Flask Security.

Create a file in your working directory named settings.py and copy the code below into the file:

KNOWN_PARTICIPANTS = {
  'twilioisthebest@twilio.com': "<YOUR_PHONE_NUMBER>",
  'cedric@twilioquestgame.com': '+15552211986',
  'twilioquestthebest@twilio.com': '+15553242003',
}

The dictionary can be modified to include different emails and phone numbers as you please. Make sure the phone numbers are in E.164 format as seen in the settings.py example above. Be sure to add your phone number to an existing item in the dictionary, or create a new item with your information. Each email address is a unique key which is helpful in our case because we want to look users up quickly in the login step.

Plan the logic of the project

The flow of logic for the project goes as follows:

  • A user from KNOWN_PARTICIPANTS will enter their email on the website homepage.
  • The Flask application sends a one time passcode to the user's phone number.
  • The user is prompted to enter the verification code they received from their phone to verify their identity to their account.
  • If the user entered the verification code correctly, they are redirected to a page to upload a .jpg or .png image of their choice.

With that said, let's start coding!

In your working directory, create a file named app.py and copy and paste the following code:

import os
from dotenv import load_dotenv
from twilio.rest import Client
from flask import Flask, request, render_template, redirect, session, url_for
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException
from werkzeug.utils import secure_filename
from werkzeug.datastructures import  FileStorage

load_dotenv()
app = Flask(__name__)
app.secret_key = 'not-so-secret-key'
app.config.from_object('settings')

UPLOAD_FOLDER = 'UPLOADS'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['UPLOAD_EXTENSIONS'] = ['jpg', 'png']

TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID')
TWILIO_AUTH_TOKEN= os.environ.get('TWILIO_AUTH_TOKEN')
VERIFY_SERVICE_SID= os.environ.get('VERIFY_SERVICE_SID')
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

KNOWN_PARTICIPANTS = app.config['KNOWN_PARTICIPANTS']

At the top of the file, we imported the necessary Python modules and libraries so that the project can load the environment variables, the list of participants from settings.py, and start the Flask app.

The Flask app will also have a secret_key for some level of security for the user session. Any random string can replace "not-so-secret-key". This is also required in our project since we need to store the users' account information and pass it along to other routes on the site using Flask's session.

Create an empty folder named "UPLOADS'' in the same flaskupload project directory. This is where all uploaded image files will be stored for the sake of simplicity in this tutorial.

Create the template folder for HTML pages

To build the UI for this project, you’ll be using Flask templates. Create a folder in the working directory named templates and create the following files inside of the folder:

  • index.html - the landing page for the user to enter their email and request a verification token.
  • verifypage.html - for the user to enter the verification code when prompted.  
  • uploadpage.html - for the user to submit a picture in .jpg or .png format.
  • success.html - page indicating the success of uploading an image!

Build the user login page

For this project, the user will go to the website and enter their username, which is an email in this case. Copy and paste the following code at the bottom of your app.py file:

@app.route('/', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        username = request.form['username']
        if username in KNOWN_PARTICIPANTS:
            session['username'] = username
            send_verification(username)
            return redirect(url_for('verify_passcode_input'))
        error = "User not found. Please try again."
        return render_template('index.html', error=error)
    return render_template('index.html')

A POST request is made to allow the participant's username to be stored in the Flask session. If the username is in the database, in this case the KNOWN_PARTICIPANTS dictionary, then the username is stored in the current Flask session and the verification token is sent to the corresponding phone number. The participant is redirected to another route where they will see a second form allowing them to submit the verification code.

Create a form that takes in a username input, as well as a button to submit. Copy and paste the barebones HTML form from my GitHub repo into the index.html file:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <h1>Login</h1>
    {% if error %}
      <p class=error><strong>Error:</strong> {{ error }}
    {% endif %}
  </head>
  <body>         
    <form method="POST">
      <div class="field">
      <label class="label">Username</label>
      <input class="input" type="text" name="username" placeholder="Username">
      </div>
      <div class="field">
        <p class="control">
          <button type="submit" class="button is-success">
            Request verification code
          </button>
        </p>
      </div>
    </form>
  </body>
</html>

Here's an example of what happens when the user enters a username that’s not in the KNOWN_PARTICIPANTS dictionary:

screenshot of localhost:5000 Login page with an error saying "User not found. Please try again."

With the form set up, it’s now time to build the send_verification function that will fire after the user submits the form.

Generate a verification code with Twilio Verify

Time for the fun part - calling the Twilio Verify API!

We want to send the verification token after the user enters a valid email in our database. Add the following code to the app.py file under the same route as the login function:

@app.route('/', methods=['GET', 'POST'])

# …

def send_verification(username):
    phone = KNOWN_PARTICIPANTS.get(username)
    client.verify \
        .services(VERIFY_SERVICE_SID) \
        .verifications \
        .create(to=phone, channel='sms')

The Twilio Client sends a verification token to the phone number associated with the username stored in the current Flask session. The specified channel in this case is SMS but it can be sent as a call by sending channel=’voice’ if you prefer.

Keep in mind that this is a simple function that sends a verification passcode and does not yet account for error handling.

Time to test it out. On the webpage, enter the email in settings.py that corresponds to your phone number. You should get an SMS with a passcode shortly.

Verify the user's phone number

We will now take the user input from a new form and make sure it is the same exact verification code that Twilio texted via SMS to the phone.

Let's start by creating the form on the HTML side. Copy and paste the HTML from my GitHub repo into the body of verifypage.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Verify your account</title>
  </head>
  <body>
    <h1 class="title">
      Please verify your account {{username}}
    </h1>
    {% if error %}
      <p class=error><strong>Error:</strong> {{ error }}
    {% endif %}
    <form method="POST">
      <div class="field">
        <label class="label">Enter the code sent to your phone number.</label>
        <input class="input" type="password" name= "verificationcode" placeholder="verificationcode">
        </p>
      </div>
      <div class="field">
        <p class="control">
          <button type="submit" class="is-success", value = "submitcode">
            Submit Verification Code
          </button>
        </p>
      </div>
    </form>
  </body>
</html>

But wait - how can we verify the 6 digit code if Twilio is the one that sends out the code? We need to define the /verifyme route and define the appropriate functions so that the user can verify the passcode.

Copy and paste the following code to the bottom of the app.py file:

@app.route('/verifyme', methods=['GET', 'POST'])
def verify_passcode_input():
    username = session['username']
    phone = KNOWN_PARTICIPANTS.get(username)
    error = None
    if request.method == 'POST':
        verification_code = request.form['verificationcode']
        if check_verification_token(phone, verification_code):
            return render_template('uploadpage.html', username = username)
        else:
            error = "Invalid verification code. Please try again."
            return render_template('verifypage.html', error=error)
    return render_template('verifypage.html', username=username)

We need to define the check_verification_token() function beneath the verify_passcode_input() code so that this function can be called within this route:

def check_verification_token(phone, token):
    check = client.verify \
        .services(VERIFY_SERVICE_SID) \
        .verification_checks \
        .create(to=phone, code=token)    
    return check.status == 'approved'

The check_verification_token() function takes in the Flask session's phone number and the verification_code that the user typed into the textbox and calls the Verify API to make sure they entered the one time passcode correctly.

So once the user submits the form, which then makes the POST request to the /verifyme route, the verify_passcode_input()  function is called. If the passcode was correct, the success page is rendered. Similar to the logic for the login page, if the participant enters an incorrect verification code, the page will refresh and show an error message. The page will also let the user enter the verification code again.

Allow users to submit a photo

At this point, the user has entered their credentials and verification code correctly. It's time to build out the page where users can drag and drop the image file. Copy and paste the HTML from my GitHub repo into the body of uploadpage.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Upload an image</title>
  </head>
  <body>
    <h1 class="title">
      {{username}}, please upload an image in .jpg or .png format
    </h1>
    {% if error %}
      <p class=error><strong>Error:</strong> {{ error }}
    {% endif %}
    <form action = "http://localhost:5000/uploader" method = "POST" 
      enctype = "multipart/form-data">
      <input type = "file" name = "file" />
      <br>
      <br>
      <input type = "submit"/>
    </form>   
  </body>
</html>

As you might have noticed, the mechanics in the uploadpage.html page are different, as you have to specify the input type that is expected in this POST request. Here, multipart/form-data is used in order to encode the data as a file element.

Once the user clicks on the Submit button, they are rerouted to the /uploader of the website where they will see a success page if they uploaded an appropriate file with either a .jpg or .png extension, or, be asked to reupload a valid file.

Here's what the error message could look like:

screenshot of localhost:5000 "uploader" page telling the email address to upload a valid file in .jpg or .png format

Go back to the app.py file to build out these two new routes so that the user can upload an image. Copy and paste the following code to the bottom of the file:

@app.route('/upload')
def upload_file():
   return render_template('uploadpage.html')
        
@app.route('/uploader', methods=['GET', 'POST'])
def submitted_file():
   username = session['username']
   if request.method == 'POST':
      f = request.files['file']
      if f and allowed_file(f.filename):
         f.save(os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(f.filename)))
         return render_template('success.html',  username=username)
      else:
         error = "Please upload a valid file."
         return render_template('uploadpage.html', username = username, error = error)

If the user makes a POST request on the uploadpage.html, then the file is stored in the Flask session's request object. Not only do we check if the user uploaded a valid .jpg or .png file and that exists, but we have to make sure that the user input is appropriate.

This is the reason we imported the werkzeug library earlier so that we can utilize the secure_filename function. Our project saves the uploaded files inside the project directory so we must take the precautions to identify file names that may harm our system.

To check the file extensions, we need to define the allowed_file function where the file name uploaded can be parsed and checked against the app.config['UPLOAD_EXTENSIONS']. Scroll up to the top of the app.py file to define a global function.

Place the following code between the definition for the first / route and the KNOWN_PARTICIPANTS:

def allowed_file(filename):
   return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['UPLOAD_EXTENSIONS']

@app.route('/', methods=['GET', 'POST'])
def login():

Once the file input has been checked and approved, the file is finally saved into the project directory's "UPLOADS" folder, as stated in app.config['UPLOAD_FOLDER'].

Another thing to consider is that in a larger scale project, you would not want to store all of your files on your system. You might want to store the files in a database or dedicated cloud storage service instead and consider the maximum size limit for the user inputs. Flask currently takes care of this for us by raising a RequestEntityTooLarge exception, but you can implement your own rules as you please.

Display a success message

You can now redirect them somewhere else as you please, but in this tutorial, you’ll redirect them to a success page. Copy and paste the HTML from my GitHub repo into the success.html file within the templates directory:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Successful Login and Picture Submission!</title>
  </head>
  <body>
    <div class="container">
      <h1 class="title">
        {{username}}'s Profile
      </h1>
      <h2 class="subtitle">
        Thanks for submitting a picture! 
      </h2>
    </div>
  </body>
</html>

Authenticate your account with Twilio Verify and submit an image

It's time to test out the app. Feel free to look at the completed code on GitHub.

Make sure that Flask is running on your terminal with flask run. Visit http://localhost:5000/ and enter any username from the defined dictionary in settings.py.

I'll use the "twilioisthebest@twilio.com" email which is the key for my own phone number for testing:

screenshot of localhost:5000 Login page

Check your phone to see the notification for the verification code provided by Twilio Verify. Seems like the code for my case was 864831.

screenshot of the Twilio Verify service sending a verification code notification on the phone

screenshot of localhost:5000 "verifyme" page prompting the user to submit their verification code

After entering the code correctly, you'll see this page:

screenshot of localhost:5000 "upload" page telling the user to please upload an image in .jpg or .png format

Select an image in .jpg or .png format. Click on the Submit button.

screenshot of localhost:5000 "uploader" page indicating the success of the uploaded file

Check your project directory and look at the UPLOADS folder, or whichever folder you created to store the pictures. You should see the image you just submitted.

screenshot of the submitted image file inside of the working project directory

What’s next for image uploading sites?

Congratulations on implementing safe practices and incorporating security measures into your project! There are plenty of ways to expand on this project and build a full fledged site to interact with users safely and avoid bad actors that you may come across.

Another way you can use Twilio Verify for authentication is to send a verification code over email using Twilio Verify and SendGrid. In that case, you would use the username in the database or any email address on the user's profile.

You can also increase the amount of safety in checking the user's input by implementing an image recognition API in your project to detect content such as NSFW photos.

Let me know what you're building in your image uploading site and how you are protecting the users in your project by reaching out to me over email!

Diane Phan is a developer on the Developer Voices team. She loves to help programmers tackle difficult challenges that might prevent them from bringing their projects to life. She can be reached at dphan [at] twilio.com or LinkedIn.