As of November 2022, Twilio no longer provides support for Authy SMS/Voice-only customers. Customers who were also using Authy TOTP or Push prior to March 1, 2023 are still supported. The Authy API is now closed to new customers and will be fully deprecated in the future.
For new development, we encourage you to use the Verify v2 API.
Existing customers will not be impacted at this time until Authy API has reached End of Life. For more information about migration, see Migrating from Authy to Verify for SMS.
Ready to add Authy user account verification to your Flask application? Don't worry, this will be the easiest thing you do all day.
Here's how it all works at a high level:
To get this done, you'll be working with the following Twilio-powered APIs:
Authy REST API
Twilio REST API
account_verification_flask/__init__.py
1from flask import Flask2from flask_login import LoginManager3from flask_sqlalchemy import SQLAlchemy4from flask_bcrypt import Bcrypt5from account_verification_flask.config import config_env_files67app = Flask(__name__)8db = SQLAlchemy()9bcrypt = Bcrypt()10login_manager = LoginManager()111213def prepare_app(14environment='development', p_db=db, p_bcrypt=bcrypt, p_login_manager=login_manager15):16app.config.from_object(config_env_files[environment])1718p_db.init_app(app)19p_bcrypt.init_app(app)20p_login_manager.init_app(app)21p_login_manager.login_view = 'register'22return app232425prepare_app()2627import account_verification_flask.views # noqa F40228
All of this can be done in under a half an hour with the simplicity and power of Authy and Twilio. Let's get started!
For this application we'll be using the The Twilio Python Helper Library and the Python Client for Authy API. We require some configuration from your side before we can begin.
Edit the DevelopmentConfig
class constant values located in the account_verification_flask/config.py
file:
1AUTHY_KEY = 'your_authy_key'23TWILIO_ACCOUNT_SID = 'your_twilio_account_sid'4TWILIO_AUTH_TOKEN = 'your_twilio_auth_token'5TWILIO_NUMBER = 'your_twilio_phone_number'67SQLALCHEMY_DATABASE_URI = 'sqlite://'8
Note that you have to replace the placeholders your_twilio_account_sid
, your_twilio_auth_token
, your_twilio_phone_number
and your_authy_key
with your information. You can find all of those parameters (and more) in your Twilio Account Console and your Authy dashboard.
account_verification_flask/config.py
1import os23from dotenv import load_dotenv45load_dotenv()67basedir = os.path.abspath(os.path.dirname(__file__))8910class DefaultConfig(object):11SECRET_KEY = os.environ.get('SECRET_KEY', 'secret-key')12SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI')13SQLALCHEMY_TRACK_MODIFICATIONS = False1415TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', None)16TWILIO_API_KEY = os.environ.get('TWILIO_API_KEY', None)17TWILIO_API_SECRET = os.environ.get('TWILIO_API_SECRET', None)18TWILIO_NUMBER = os.environ.get('TWILIO_NUMBER', None)19AUTHY_API_KEY = os.environ.get('AUTHY_API_KEY', None)2021DEBUG = False222324class DevelopmentConfig(DefaultConfig):25SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'dev.sqlite')26DEBUG = True272829class TestConfig(DefaultConfig):30SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'31SQLALCHEMY_ECHO = True32DEBUG = True33TESTING = True34WTF_CSRF_ENABLED = False353637config_env_files = {38'testing': 'account_verification_flask.config.TestConfig',39'development': 'account_verification_flask.config.DevelopmentConfig',40'production': 'account_verification_flask.config.Default',41}42
Now that we've got the setup boilerplate out of the way, let's take a look at the User
model.
We'll be using the Flask-Login library for our user session management. To integrate this library into our code we need to implement a few properties and methods, then we're good to go.
Note the new variable authy_user_id
, which is implemented for storing the user's Authy identification token.
account_verification_flask/models/models.py
1# from flask.ext.login import UserMixin2from account_verification_flask import db, bcrypt345class User(db.Model):6__tablename__ = "users"78id = db.Column(db.Integer, primary_key=True)9name = db.Column(db.String, nullable=False)10email = db.Column(db.String, nullable=False)11password = db.Column(db.String)12phone_number = db.Column(db.String, nullable=False)13country_code = db.Column(db.String, nullable=False)14phone_number_confirmed = db.Column(db.Boolean, nullable=False, default=False)15authy_user_id = db.Column(db.String, nullable=True)1617def __init__(self, name, email, password, phone_number, country_code):18self.name = name19self.email = email20self.password = bcrypt.generate_password_hash(password)21self.phone_number = phone_number22self.country_code = country_code23self.phone_number_confirmed = False2425def is_authenticated(self):26return True2728def is_active(self):29return True3031def is_anonymous(self):32return False3334def get_id(self):35return str(self.id)3637def __str__(self):38return self.name3940def __repr__(self):41return f'<User: {self.name}>'42
Next we're going to visit the registration form on the client side.
In order to validate the user's account and register the user, we need a mobile number with a country code. We can then use Authy to send a verification code via SMS.
In this example we're validating and rendering the forms with the WTForms library. This allows us to define the forms as classes inside Python.
account_verification_flask/templates/register.html
1{% extends "layout.html" %}23{% block content %}45<h1>We're going to be *BEST* friends</h1>6<p> Thanks for your interest in signing up! Can you tell us a bit about yourself?</p>789<form method="POST" class="form-horizontal" role="form">10{% from "_formhelpers.html" import render_errors, render_field %}11{{ form.csrf_token }}12{{ render_errors(form) }}13<hr/>1415{{ render_field(form.name, placeholder='Anakin Skywalker') }}16{{ render_field(form.email, placeholder='darth@vader.com') }}17{{ render_field(form.password) }}18{{ render_field(form.country_code, id="authy-countries" ) }}19{{ render_field(form.phone_number , type='number') }}2021<div class="form-group">22<div class="col-md-offset-2 col-md-10">23<input type="submit" class="btn btn-primary" value="Sign Up" />24</div>25</div>26</form>2728{% endblock %}29
That's it for the client side. Now let's look at what happens when the user submits the form.
Next our controller stores the new user, registers them with Authy's API, and requests a new verification code.
account_verification_flask/views.py
1from flask import request, flash, g2from flask_login import login_user, logout_user, current_user3from account_verification_flask import app, db, login_manager4from account_verification_flask.forms.forms import (5RegisterForm,6ResendCodeForm,7VerifyCodeForm,8)9from account_verification_flask.models.models import User10from account_verification_flask.services.authy_services import AuthyServices11from account_verification_flask.services.twilio_services import TwilioServices12from account_verification_flask.utilities import User_Already_Confirmed13from account_verification_flask.utilities.view_helpers import view, redirect_to14import account_verification_flask.utilities151617@app.route('/')18@app.route('/home')19def home():20return view('index')212223@app.route('/register', methods=["GET", "POST"])24def register():25form = RegisterForm()26if request.method == 'POST':27if form.validate_on_submit():2829if User.query.filter(User.email == form.email.data).count() > 0:30form.email.errors.append(31account_verification_flask.utilities.User_Email_Already_In_Use32)33return view('register', form)3435user = User(36name=form.name.data,37email=form.email.data,38password=form.password.data,39country_code=form.country_code.data,40phone_number=form.phone_number.data,41)42db.session.add(user)43db.session.commit()4445authy_services = AuthyServices()46if authy_services.request_phone_confirmation_code(user):47db.session.commit()48flash(account_verification_flask.utilities.Verification_Code_Sent)49return redirect_to('verify', email=form.email.data)5051form.email.errors.append(52account_verification_flask.utilities.Verification_Code_Not_Sent53)5455else:56return view('register', form)5758return view('register', form)596061@app.route('/verify', methods=["GET", "POST"])62@app.route('/verify/<email>', methods=["GET"])63def verify():64form = VerifyCodeForm()65if request.method == 'POST':66if form.validate_on_submit():67user = User.query.filter(User.email == form.email.data).first()6869if user is None:70form.email.errors.append(71account_verification_flask.utilities.User_Not_Found_For_Given_Email72)73return view('verify_registration_code', form)7475if user.phone_number_confirmed:76form.email.errors.append(User_Already_Confirmed)77return view('verify_registration_code', form)7879authy_services = AuthyServices()80if authy_services.confirm_phone_number(user, form.verification_code.data):81user.phone_number_confirmed = True82db.session.commit()83login_user(user, remember=True)84twilio_services = TwilioServices()85twilio_services.send_registration_success_sms(86"+{0}{1}".format(user.country_code, user.phone_number)87)88return redirect_to('status')89else:90form.email.errors.append(91account_verification_flask.utilities.Verification_Unsuccessful92)93return view('verify_registration_code', form)94else:95form.email.data = request.args.get('email')96return view('verify_registration_code', form)979899@app.route('/resend', methods=["GET", "POST"])100@app.route('/resend/<email>', methods=["GET"])101def resend(email=""):102form = ResendCodeForm()103104if request.method == 'POST':105if form.validate_on_submit():106user = User.query.filter(User.email == form.email.data).first()107108if user is None:109form.email.errors.append(110account_verification_flask.utilities.User_Not_Found_For_Given_Email111)112return view('resend_confirmation_code', form)113114if user.phone_number_confirmed:115form.email.errors.append(116account_verification_flask.utilities.User_Already_Confirmed117)118return view('resend_confirmation_code', form)119authy_services = AuthyServices()120if authy_services.request_phone_confirmation_code(user):121flash(account_verification_flask.utilities.Verification_Code_Resent)122return redirect_to('verify', email=form.email.data)123else:124form.email.errors.append(125account_verification_flask.utilities.Verification_Code_Not_Sent126)127else:128form.email.data = email129130return view('resend_confirmation_code', form)131132133@app.route('/status')134def status():135return view('status')136137138@app.route('/logout', methods=["POST"])139def logout():140logout_user()141return redirect_to('home')142143144# controller utils145@app.before_request146def before_request():147g.user = current_user148149150@login_manager.user_loader151def load_user(user_id):152try:153return User.query.get(user_id)154except Exception:155return None156
Next we'll set up our application to complete our user verification.
On the server we first check that the email belongs to a user that we haven't yet verified.
The process then has two critical steps:
After that (assuming a success!) we redirect the user to a success page.
account_verification_flask/views.py
1from flask import request, flash, g2from flask_login import login_user, logout_user, current_user3from account_verification_flask import app, db, login_manager4from account_verification_flask.forms.forms import (5RegisterForm,6ResendCodeForm,7VerifyCodeForm,8)9from account_verification_flask.models.models import User10from account_verification_flask.services.authy_services import AuthyServices11from account_verification_flask.services.twilio_services import TwilioServices12from account_verification_flask.utilities import User_Already_Confirmed13from account_verification_flask.utilities.view_helpers import view, redirect_to14import account_verification_flask.utilities151617@app.route('/')18@app.route('/home')19def home():20return view('index')212223@app.route('/register', methods=["GET", "POST"])24def register():25form = RegisterForm()26if request.method == 'POST':27if form.validate_on_submit():2829if User.query.filter(User.email == form.email.data).count() > 0:30form.email.errors.append(31account_verification_flask.utilities.User_Email_Already_In_Use32)33return view('register', form)3435user = User(36name=form.name.data,37email=form.email.data,38password=form.password.data,39country_code=form.country_code.data,40phone_number=form.phone_number.data,41)42db.session.add(user)43db.session.commit()4445authy_services = AuthyServices()46if authy_services.request_phone_confirmation_code(user):47db.session.commit()48flash(account_verification_flask.utilities.Verification_Code_Sent)49return redirect_to('verify', email=form.email.data)5051form.email.errors.append(52account_verification_flask.utilities.Verification_Code_Not_Sent53)5455else:56return view('register', form)5758return view('register', form)596061@app.route('/verify', methods=["GET", "POST"])62@app.route('/verify/<email>', methods=["GET"])63def verify():64form = VerifyCodeForm()65if request.method == 'POST':66if form.validate_on_submit():67user = User.query.filter(User.email == form.email.data).first()6869if user is None:70form.email.errors.append(71account_verification_flask.utilities.User_Not_Found_For_Given_Email72)73return view('verify_registration_code', form)7475if user.phone_number_confirmed:76form.email.errors.append(User_Already_Confirmed)77return view('verify_registration_code', form)7879authy_services = AuthyServices()80if authy_services.confirm_phone_number(user, form.verification_code.data):81user.phone_number_confirmed = True82db.session.commit()83login_user(user, remember=True)84twilio_services = TwilioServices()85twilio_services.send_registration_success_sms(86"+{0}{1}".format(user.country_code, user.phone_number)87)88return redirect_to('status')89else:90form.email.errors.append(91account_verification_flask.utilities.Verification_Unsuccessful92)93return view('verify_registration_code', form)94else:95form.email.data = request.args.get('email')96return view('verify_registration_code', form)979899@app.route('/resend', methods=["GET", "POST"])100@app.route('/resend/<email>', methods=["GET"])101def resend(email=""):102form = ResendCodeForm()103104if request.method == 'POST':105if form.validate_on_submit():106user = User.query.filter(User.email == form.email.data).first()107108if user is None:109form.email.errors.append(110account_verification_flask.utilities.User_Not_Found_For_Given_Email111)112return view('resend_confirmation_code', form)113114if user.phone_number_confirmed:115form.email.errors.append(116account_verification_flask.utilities.User_Already_Confirmed117)118return view('resend_confirmation_code', form)119authy_services = AuthyServices()120if authy_services.request_phone_confirmation_code(user):121flash(account_verification_flask.utilities.Verification_Code_Resent)122return redirect_to('verify', email=form.email.data)123else:124form.email.errors.append(125account_verification_flask.utilities.Verification_Code_Not_Sent126)127else:128form.email.data = email129130return view('resend_confirmation_code', form)131132133@app.route('/status')134def status():135return view('status')136137138@app.route('/logout', methods=["POST"])139def logout():140logout_user()141return redirect_to('home')142143144# controller utils145@app.before_request146def before_request():147g.user = current_user148149150@login_manager.user_loader151def load_user(user_id):152try:153return User.query.get(user_id)154except Exception:155return None156
What happens if the message was never sent, didn't arrive, or can't be found? Let's look at how to handle those scenarios next.
The form for re-sending the code is a single line, so let's skip that detail for this tutorial. Instead, let's just take a look at the controller function for resending verifications.
This controller loads the User
associated with the request and then uses the same Authy API method we used earlier to resend the code. Pretty straightforward, right?
account_verification_flask/views.py
1from flask import request, flash, g2from flask_login import login_user, logout_user, current_user3from account_verification_flask import app, db, login_manager4from account_verification_flask.forms.forms import (5RegisterForm,6ResendCodeForm,7VerifyCodeForm,8)9from account_verification_flask.models.models import User10from account_verification_flask.services.authy_services import AuthyServices11from account_verification_flask.services.twilio_services import TwilioServices12from account_verification_flask.utilities import User_Already_Confirmed13from account_verification_flask.utilities.view_helpers import view, redirect_to14import account_verification_flask.utilities151617@app.route('/')18@app.route('/home')19def home():20return view('index')212223@app.route('/register', methods=["GET", "POST"])24def register():25form = RegisterForm()26if request.method == 'POST':27if form.validate_on_submit():2829if User.query.filter(User.email == form.email.data).count() > 0:30form.email.errors.append(31account_verification_flask.utilities.User_Email_Already_In_Use32)33return view('register', form)3435user = User(36name=form.name.data,37email=form.email.data,38password=form.password.data,39country_code=form.country_code.data,40phone_number=form.phone_number.data,41)42db.session.add(user)43db.session.commit()4445authy_services = AuthyServices()46if authy_services.request_phone_confirmation_code(user):47db.session.commit()48flash(account_verification_flask.utilities.Verification_Code_Sent)49return redirect_to('verify', email=form.email.data)5051form.email.errors.append(52account_verification_flask.utilities.Verification_Code_Not_Sent53)5455else:56return view('register', form)5758return view('register', form)596061@app.route('/verify', methods=["GET", "POST"])62@app.route('/verify/<email>', methods=["GET"])63def verify():64form = VerifyCodeForm()65if request.method == 'POST':66if form.validate_on_submit():67user = User.query.filter(User.email == form.email.data).first()6869if user is None:70form.email.errors.append(71account_verification_flask.utilities.User_Not_Found_For_Given_Email72)73return view('verify_registration_code', form)7475if user.phone_number_confirmed:76form.email.errors.append(User_Already_Confirmed)77return view('verify_registration_code', form)7879authy_services = AuthyServices()80if authy_services.confirm_phone_number(user, form.verification_code.data):81user.phone_number_confirmed = True82db.session.commit()83login_user(user, remember=True)84twilio_services = TwilioServices()85twilio_services.send_registration_success_sms(86"+{0}{1}".format(user.country_code, user.phone_number)87)88return redirect_to('status')89else:90form.email.errors.append(91account_verification_flask.utilities.Verification_Unsuccessful92)93return view('verify_registration_code', form)94else:95form.email.data = request.args.get('email')96return view('verify_registration_code', form)979899@app.route('/resend', methods=["GET", "POST"])100@app.route('/resend/<email>', methods=["GET"])101def resend(email=""):102form = ResendCodeForm()103104if request.method == 'POST':105if form.validate_on_submit():106user = User.query.filter(User.email == form.email.data).first()107108if user is None:109form.email.errors.append(110account_verification_flask.utilities.User_Not_Found_For_Given_Email111)112return view('resend_confirmation_code', form)113114if user.phone_number_confirmed:115form.email.errors.append(116account_verification_flask.utilities.User_Already_Confirmed117)118return view('resend_confirmation_code', form)119authy_services = AuthyServices()120if authy_services.request_phone_confirmation_code(user):121flash(account_verification_flask.utilities.Verification_Code_Resent)122return redirect_to('verify', email=form.email.data)123else:124form.email.errors.append(125account_verification_flask.utilities.Verification_Code_Not_Sent126)127else:128form.email.data = email129130return view('resend_confirmation_code', form)131132133@app.route('/status')134def status():135return view('status')136137138@app.route('/logout', methods=["POST"])139def logout():140logout_user()141return redirect_to('home')142143144# controller utils145@app.before_request146def before_request():147g.user = current_user148149150@login_manager.user_loader151def load_user(user_id):152try:153return User.query.get(user_id)154except Exception:155return None156
Let's take a step back and see how we can use Authy to resend a verification code to an unverified user.
In order to end up with a cleaner and decoupled design we'll encapsulate all of Authy's related features in an AuthyService
. This class will hold a shared class instance of the AuthyApiClient
class.
Once the user has an authyId
we can send a verification code to that user's mobile phone.
account_verification_flask/services/authy_services.py
1import account_verification_flask.utilities2from account_verification_flask.utilities.settings import AuthySettings3from authy.api import AuthyApiClient456class AuthyServices:7authy_client = None89def __init__(self):10if AuthyServices.authy_client is None:11AuthyServices.authy_client = AuthyApiClient(AuthySettings.key())1213def request_phone_confirmation_code(self, user):14if user is None:15raise ValueError(account_verification_flask.utilities.User_Id_Not_Found)1617if user.authy_user_id is None:18self._register_user_under_authy(user)1920sms = self.authy_client.users.request_sms(user.authy_user_id, {'force': True})21return not sms.ignored()2223def confirm_phone_number(self, user, verification_code):24if user is None:25raise ValueError(account_verification_flask.utilities.User_Id_Not_Found)2627verification = self.authy_client.tokens.verify(28user.authy_user_id, verification_code29)30return verification.ok()3132def _register_user_under_authy(self, user):33authy_user = self.authy_client.users.create(34user.email, user.phone_number, user.country_code35)36if authy_user.ok:37user.authy_user_id = authy_user.id38
When our user is created successfully via the form we have implemented, we send a token to the user's mobile phone asking them to verify their account in our controller. When the code is sent, we redirect our users to another page where they can enter the received token, completing the verification process.
Authy provides us with a tokens.verify
method that allows us to pass a user id
and token
. In this case we just need to check that the API request was successful and, if so, set a verified
flag on the user.
account_verification_flask/services/authy_services.py
1import account_verification_flask.utilities2from account_verification_flask.utilities.settings import AuthySettings3from authy.api import AuthyApiClient456class AuthyServices:7authy_client = None89def __init__(self):10if AuthyServices.authy_client is None:11AuthyServices.authy_client = AuthyApiClient(AuthySettings.key())1213def request_phone_confirmation_code(self, user):14if user is None:15raise ValueError(account_verification_flask.utilities.User_Id_Not_Found)1617if user.authy_user_id is None:18self._register_user_under_authy(user)1920sms = self.authy_client.users.request_sms(user.authy_user_id, {'force': True})21return not sms.ignored()2223def confirm_phone_number(self, user, verification_code):24if user is None:25raise ValueError(account_verification_flask.utilities.User_Id_Not_Found)2627verification = self.authy_client.tokens.verify(28user.authy_user_id, verification_code29)30return verification.ok()3132def _register_user_under_authy(self, user):33authy_user = self.authy_client.users.create(34user.email, user.phone_number, user.country_code35)36if authy_user.ok:37user.authy_user_id = authy_user.id38
That's it for token verification! Let's provide a nice user onboarding experience, and send a confirmation message to our new user.
Just as we did for our Authy client, we create a single instance of the Twilio REST API helper. It will be called twilio_client
in this example.
After that, it's straightforward - send an SMS using the Twilio Python helper library to the same number we used in messages.create().
account_verification_flask/services/twilio_services.py
1import account_verification_flask.utilities2from account_verification_flask.utilities.settings import TwilioSettings3from twilio.rest import Client456class TwilioServices:7twilio_client = None89def __init__(self):10if TwilioServices.twilio_client is None:11TwilioServices.twilio_client = Client(12TwilioSettings.api_key(),13TwilioSettings.api_secret(),14TwilioSettings.account_sid(),15)1617def send_registration_success_sms(self, to_number):18self.twilio_client.messages.create(19body=account_verification_flask.utilities.Signup_Complete,20to=to_number,21from_=TwilioSettings.phone_number(),22)23
Congratulations! You've successfully verified new user accounts with Authy. Where can we take it from here?
In one tutorial, we've implemented account verification with Authy and Twilio, allowing your users to confirm accounts with their phone number! Now it's on you - let us know what you build on Twitter, and check out these other tutorials:
Use Twilio to automate the process of reaching out to your customers in advance of an upcoming appointment.
Two-Factor Authentication with Authy
Use Twilio and Twilio-powered Authy OneTouch to implement two-factor authentication (2FA) in your web app.