SMS Two-Factor Authentication with Python and Flask

This Flask application example demonstrates how to implement an SMS two-factor authentication using Twilio.

To follow along with this tutorial yourself, please clone the repo from GitHub and follow the instructions there on starting the local server.

Adding two-factor authentication (2FA) to your web application increases the security of your user's data. Multi-factor authentication with Twilio will help you determine a user's identity in two steps:

  1. First we validate the user with an email and password
  2. Second we validate the user using his or her mobile device, by sending a one-time verification code

Once our user enters the verification code, we know they have received the SMS and are indeed who they say they are. This tutorial will walk you through a standard SMS implementation.

Loading Code Samples...
Language
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
sms2fa_flask/confirmation_sender.py
Initialize Twilio client, Generate and send validation code

sms2fa_flask/confirmation_sender.py

For a more advanced - and more secure - integration using Authy One-Touch, checkout this tutorial.

Intuit uses Twilio SMS to protect 1M+ businesses from online security threats. Read why they chose Twilio.

Generate a Verification Code

Once our user logs in we need to send them the one-time verification code.

To generate our verification code we use random.randrange which can take a range as an argument. Let's send them a 6-digit verification code, somewhere between 100000 and 999999.

Loading Code Samples...
Language
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
sms2fa_flask/confirmation_sender.py
Generate a Verification Code

sms2fa_flask/confirmation_sender.py

Next, let's take a look at how we would send this in an SMS with Twilio.

Send a Verification Code

The Twilio Python helper library allows us to easily send an SMS.

First we have to create an instance of a Twilio Rest Client with our credentials. Then all we have to do, to be able to send an SMS using the REST API, is to call client.messages.create() with the necessary parameters.

You can find the necessary credentials in the Twilio Console.

Loading Code Samples...
Language
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
sms2fa_flask/confirmation_sender.py
Send a Verification Code

sms2fa_flask/confirmation_sender.py

Now that we know how to generate the verification code and send it, let's now look at how to kick off the signup process.

Register a User

When a user signs up for your website, this controller creates the user and sends them the generated verification code.

In order to do two-factor authentication we need to make sure we ask for the user's phone number.

Let's see how to implement the send_confirmation_code function.

Loading Code Samples...
Language
from flask import render_template, request, url_for, flash, redirect, session
from flask import abort
from flask.ext.login import login_required
from flask.ext.login import login_user
from flask.ext.login import logout_user
from . import app
from .models import User
from .forms import LoginForm, SignUpForm
from .confirmation_sender import send_confirmation_code


@app.route('/')
def root():
    return render_template('index.html')


@app.route('/secret-page')
@login_required
def secret_page():
    return render_template('secrets.html')


@app.route('/sign-up', methods=['GET', 'POST'])
def sign_up():
    form = SignUpForm()
    if form.validate_on_submit():
        user = User.save_from_dict(form.as_dict)
        session['user_email'] = user.email
        send_confirmation_code(user.international_phone_number)
        return redirect(url_for('confirmation'))
    return render_template('signup.html', form=form)


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

    if form.validate_on_submit():
        user = User.query.get(form.email.data)
        if user and user.is_password_valid(form.password.data):
            session['user_email'] = user.email
            return redirect(url_for('confirmation'))
        flash('Wrong user/password.', 'error')

    send_confirmation_code(user.international_phone_number)
    return render_template('sign_in.html', form=form)


@app.route('/confirmation', methods=['GET', 'POST'])
def confirmation():
    user = User.query.get(session.get('user_email', '')) or abort(401)

    if request.method == 'POST':
        if request.form['verification_code'] == session['verification_code']:
            login_user(user)
            return redirect(url_for('secret_page'))
        flash('Wrong code. Please try again.', 'error')

    return render_template('confirmation.html', user=user)


@app.route('/logout')
def logout():
    logout_user()
    session.clear()
    return redirect(url_for('root'))
sms2fa_flask/views.py
Flask route for signup form

sms2fa_flask/views.py

Now let's take a closer at how to proceed with the 2-step verification.

Putting It All Together

Using the building blocks we've created in the previous steps we can now put it all together.

Note that we are using the Flask-Session extension for the storage of the generated code instead of putting it in the user session.  User sessions in Flask are not the proper area to store sensitive information, and secrets can be extracted from the browser console.  At a minimum, if you're going to store the validation code on the client side, use encrypted sessions with something like It's Dangerous, or use a server side solution like we're demonstrating here.

Loading Code Samples...
Language
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
sms2fa_flask/confirmation_sender.py
Send the confirmation code and save in the session

sms2fa_flask/confirmation_sender.py

And now, a drumroll for the second step of the two-step authentication implementation...

Implementing the 2-Step Verification

When the user receives an SMS with the verification code it's on us to ensure the given code is valid.

This validation is achieved by comparing the user's session verification code with the verification code the user inputs on the form.

Confirm Verification Code

If the validation was successful the application allows the user to have access to the protected content we shielded in this process. Otherwise, the application will prompt for the verification code once again.

Loading Code Samples...
Language
from flask import render_template, request, url_for, flash, redirect, session
from flask import abort
from flask.ext.login import login_required
from flask.ext.login import login_user
from flask.ext.login import logout_user
from . import app
from .models import User
from .forms import LoginForm, SignUpForm
from .confirmation_sender import send_confirmation_code


@app.route('/')
def root():
    return render_template('index.html')


@app.route('/secret-page')
@login_required
def secret_page():
    return render_template('secrets.html')


@app.route('/sign-up', methods=['GET', 'POST'])
def sign_up():
    form = SignUpForm()
    if form.validate_on_submit():
        user = User.save_from_dict(form.as_dict)
        session['user_email'] = user.email
        send_confirmation_code(user.international_phone_number)
        return redirect(url_for('confirmation'))
    return render_template('signup.html', form=form)


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

    if form.validate_on_submit():
        user = User.query.get(form.email.data)
        if user and user.is_password_valid(form.password.data):
            session['user_email'] = user.email
            return redirect(url_for('confirmation'))
        flash('Wrong user/password.', 'error')

    send_confirmation_code(user.international_phone_number)
    return render_template('sign_in.html', form=form)


@app.route('/confirmation', methods=['GET', 'POST'])
def confirmation():
    user = User.query.get(session.get('user_email', '')) or abort(401)

    if request.method == 'POST':
        if request.form['verification_code'] == session['verification_code']:
            login_user(user)
            return redirect(url_for('secret_page'))
        flash('Wrong code. Please try again.', 'error')

    return render_template('confirmation.html', user=user)


@app.route('/logout')
def logout():
    logout_user()
    session.clear()
    return redirect(url_for('root'))
sms2fa_flask/views.py
Flask route for confirming the verification code

sms2fa_flask/views.py

That's it! We've just implemented SMS Two-Factor Authentication that you can now use in your applications!

Where to next?

If you're a Python developer working with Twilio, you're going to want to eventually check out these other excellent tutorials:

Automated Survey

Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.

Click to Call

Click-to-call enables your company to convert web traffic into phone calls with the click of a button. Learn how to implement it in minutes.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!

Jose Oliveros
David Prothero
Paul Kamp
Andrew Baker
Agustin Camino
Samuel Mendes

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
from flask import render_template, request, url_for, flash, redirect, session
from flask import abort
from flask.ext.login import login_required
from flask.ext.login import login_user
from flask.ext.login import logout_user
from . import app
from .models import User
from .forms import LoginForm, SignUpForm
from .confirmation_sender import send_confirmation_code


@app.route('/')
def root():
    return render_template('index.html')


@app.route('/secret-page')
@login_required
def secret_page():
    return render_template('secrets.html')


@app.route('/sign-up', methods=['GET', 'POST'])
def sign_up():
    form = SignUpForm()
    if form.validate_on_submit():
        user = User.save_from_dict(form.as_dict)
        session['user_email'] = user.email
        send_confirmation_code(user.international_phone_number)
        return redirect(url_for('confirmation'))
    return render_template('signup.html', form=form)


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

    if form.validate_on_submit():
        user = User.query.get(form.email.data)
        if user and user.is_password_valid(form.password.data):
            session['user_email'] = user.email
            return redirect(url_for('confirmation'))
        flash('Wrong user/password.', 'error')

    send_confirmation_code(user.international_phone_number)
    return render_template('sign_in.html', form=form)


@app.route('/confirmation', methods=['GET', 'POST'])
def confirmation():
    user = User.query.get(session.get('user_email', '')) or abort(401)

    if request.method == 'POST':
        if request.form['verification_code'] == session['verification_code']:
            login_user(user)
            return redirect(url_for('secret_page'))
        flash('Wrong code. Please try again.', 'error')

    return render_template('confirmation.html', user=user)


@app.route('/logout')
def logout():
    logout_user()
    session.clear()
    return redirect(url_for('root'))
from twilio.rest import Client
from . import app
from flask import session
import random


def send_confirmation_code(to_number):
    verification_code = generate_code()
    send_sms(to_number, verification_code)
    session['verification_code'] = verification_code
    return verification_code


def generate_code():
    return str(random.randrange(100000, 999999))


def send_sms(to_number, body):
    account_sid = app.config['TWILIO_ACCOUNT_SID']
    auth_token = app.config['TWILIO_AUTH_TOKEN']
    twilio_number = app.config['TWILIO_NUMBER']
    client = Client(account_sid, auth_token)
    client.api.messages.create(to_number,
                           from_=twilio_number,
                           body=body)
from flask import render_template, request, url_for, flash, redirect, session
from flask import abort
from flask.ext.login import login_required
from flask.ext.login import login_user
from flask.ext.login import logout_user
from . import app
from .models import User
from .forms import LoginForm, SignUpForm
from .confirmation_sender import send_confirmation_code


@app.route('/')
def root():
    return render_template('index.html')


@app.route('/secret-page')
@login_required
def secret_page():
    return render_template('secrets.html')


@app.route('/sign-up', methods=['GET', 'POST'])
def sign_up():
    form = SignUpForm()
    if form.validate_on_submit():
        user = User.save_from_dict(form.as_dict)
        session['user_email'] = user.email
        send_confirmation_code(user.international_phone_number)
        return redirect(url_for('confirmation'))
    return render_template('signup.html', form=form)


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

    if form.validate_on_submit():
        user = User.query.get(form.email.data)
        if user and user.is_password_valid(form.password.data):
            session['user_email'] = user.email
            return redirect(url_for('confirmation'))
        flash('Wrong user/password.', 'error')

    send_confirmation_code(user.international_phone_number)
    return render_template('sign_in.html', form=form)


@app.route('/confirmation', methods=['GET', 'POST'])
def confirmation():
    user = User.query.get(session.get('user_email', '')) or abort(401)

    if request.method == 'POST':
        if request.form['verification_code'] == session['verification_code']:
            login_user(user)
            return redirect(url_for('secret_page'))
        flash('Wrong code. Please try again.', 'error')

    return render_template('confirmation.html', user=user)


@app.route('/logout')
def logout():
    logout_user()
    session.clear()
    return redirect(url_for('root'))