How to develop two factor authentication in python Flask

In this post I am going to develop a simple Flask login page with two-factor athentication process using CronDock sms mini app in python

In the previous article I discussed how you can send text messages using CronDock sms mini app. Also in the "how to develop simple two factor authentication in python" Article I discussed how you can develop a one click 2FA system using CronDock sms2fa mini app. In this article, I want to talk about developing Two-Factor-Authentication (2FA) in python by generating a random number and send it to the user through text message and then ask the user to input that number. I am going to use python Flask again for this example, but you can use framework and programing language of your choice for this.

As we discussed in the previous articles the idea behind two-factor authentication is to "authenticate" the user using two unrelated methods. In most cases one of the methods being password, and the other one could be anything from a button on a cell phone app, a call, text message, QR code, etc.

In this article, I am going to describe one of the most popular methods for 2FA, which is sending a randomly generated number to the user via text message (SMS). For this example, as I did in the my other article , I am going to build a simple login page in flask first. After user passes the username/password wall, we will generate a random number for the session and pass it to the user's cell phone number via text message using CronDock sms mini app. We then redirect the user to another page and will ask them to input the number they have received via SMS, and if they enter the correct number, they will be logged in. Please keep in mind that provided code is not suitable for production environment.

So let's start with installing the packages:

 python -m pip install flask flask-login flask-wtf WTForms pyjwt Flask-Session
               

Again we are going to use "SQLite" for our database, and we are going to need two tables for this example:

  • users: for each user it keeps the username, password and phone number
  • two_factor_authentication_codes: keeps track of the codes sent to the phone numbers

To create the database and the tables, you can do:

sqlite3 example.db
>
pip install Flask-Session


create table users(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username text not null,
password text not null,
phone text not null
);

create table two_factor_authentication_codes(
phone text not null,
code INTEGER not null
);

insert into users (username, password, phone) values('testuser','ABC123', '18009998877');

.q
               

Note that I am also adding a user to the users table here. Keep in mind that you should also include the country code of the phone numbers. Next, we need to create a template HTML page for our flask app, this is similar to template we had in the previous article, let's call it html_template.html:

html_template.html:
<!DOCTYPE html>
<html lang="en">
<body>
     {% with messages = get_flashed_messages(with_categories=True)%}
      {% if messages %}
       {% for category, message in messages %}
        <div class="alert alert-{{ category }}">
         <h5>{{ message }}<h5>
        </div>
       {% endfor %}
       {% else %}
        {% block info %}
        {% endblock %}
      {% endif %}
     {% endwith %}
</body>
</html>
              

As discussed in the other article, in this template we show any messages that are produced by flash command in the flask app. We also need a login page, which we call it login.html, again this is similar to the same page we had in the other article:

login.html:
{% extends "html_template.html" %}
{% block info %}
<form method='POST' action="">
 <fieldset class="form-group">
  <h1>Log In</h1>
  {{ form.hidden_tag() }}
  <div class="form-group">
   {{ form.username.label(class="form-control-label")}}
   {{ form.username(class="form-control form-control-lg")}}
  </div>
  <div class="form-group">
   {{ form.password.label(class="form-control-label")}}
   {{ form.password(class="form-control form-control-lg")}}
  </div>
 </fieldset>
 <div class="form-group">
  {{ form.submit(class="btn btn-outline-info")}}
 </div>
</form>
{% endblock info %}
               

Again as discussed in the other article, in the login page, we have a form with two text fields: username and password, and a submit button

This time we also need a page for the user to enter the number they receive on their phone number. Let's call this one two_factor_auth.html,

two_factor_auth.html:
{% extends "html_template.html" %}
{% block info %}
<form method='POST' action="">
 <fieldset class="form-group">
  <h1>Two-Factor Authentication</h1>
  {{ form.hidden_tag() }}
  <div class="form-group">
   {{ form.code.label(class="form-control-label")}}
   {{ form.code(class="form-control form-control-lg")}}
  </div>
 </fieldset>
 <div class="form-group">
  {{ form.submit(class="btn btn-outline-info")}}
 </div>
</form>
{% endblock info %}
               

The two_factor_auth.html page also have a form with one text filed for the code and a submit button, which we discuss below.

Next we have to create the FlaskForm class for the above forms. So I call the class for the first form SignInForm which is going to need 3 fields: username, password and a submit field. And the class for the second form is called TwoFactorAuthForm and this one has one string field for the code and a submit filed again. I added these classes to the forms.py file as shown below:

forms.py
 from flask_wtf import FlaskForm
 from wtforms import StringField, PasswordField, SubmitField
 from wtforms.validators import DataRequired
 import sqlite3
 class SignInForm(FlaskForm):
     username = StringField('Username',validators=[DataRequired()])
     password = PasswordField('Password',validators=[DataRequired()])
     submit = SubmitField('Login')

 class TwoFactorAuthForm(FlaskForm):
     code = StringField('Code',validators=[DataRequired()])
     submit = SubmitField('Authenticate')
               

For the main flask app, which we again call it flask_app.py, I will explain different sections of it one by one. First, imports and top definitions:

flask_app.py
from flask import Flask, url_for, redirect, session
from flask import render_template, flash, request, jsonify
import sqlite3
from flask_login import login_user, UserMixin, LoginManager
from flask_session import Session
from forms import SignInForm, TwoFactorAuthForm
import time
import requests
import json
import jwt
from random import randint

app = Flask(__name__, template_folder='.')
app.debug=True
app.config['SECRET_KEY'] = 'some-secret-value'
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)

login_manager = LoginManager(app)
login_manager.login_view = "login"
 ...
              

As usual, we are importing some methods from flask framework. We are also using some classes and methods from flask_login to handle the login and firs part of two factor authentication process. SignInForm and TwoFactorAuthForm classes are being imported the forms.py file. We are also going to use a couple of other libraries, including sqlite3 (to communicate with our database), requests (to send HTTP requests to CronDock api) and jwt (to encode/decode json tokens). One more framework that we added comparing the previous article is Session from flask_session. Sessions basically help us to move data between pages, which we use to move user information between the login page and the two factor authentication page.

Next, we are creating a Flask app, and setting a SECRET_KEY for it. This time we also set some configuration for the sessions. Including setting the SESSION_TYPE to filesystem. Note that there are other options for this including redis or mongodb if you are using those frameworks for caching data. We also have some definitions for login_manager that handles the login process.

Same as before we are going to define a User class like this:

flask_app.py
 ...

class User(UserMixin):
    def __init__(self, id, username, password, phone):
         self.id = id
         self.username = username
         self.password = password
         self.phone = phone
         self.authenticated = False
    def is_anonymous(self):
         return False
    def is_authenticated(self):
         return self.authenticated
    def is_active(self):
         return True
    def get_id(self):
         return self.id

...
              

Above is a simple user class which stores username, password and phone number of the users. Next, login_manager.user_loader:

flask_app.py
...

@login_manager.user_loader
def load_user(username=None):
    conn = sqlite3.connect('example.db')
    curs = conn.cursor()
    curs.execute("SELECT * from users where username = (?)",[username])
    lu = curs.fetchone()
    if lu is None:
      return None
    else:
        return User(int(lu[0]), lu[1], lu[2], lu[3])

...
              

This method receives a username and loads the user records from users table of our database (example.db). If such a user exists, the method will return a User class based on it, otherwise it will return None.

Next is a function that sends the 2 factor authentication text message (which includes the random code) via SMS to the user :

flask_app.py
...

def send_2_factor_authentication(phone_number, code):
    r = requests.post('https://api.crondock.com/run/sms/', json={
        "data": {
            "to_phone": phone_number,
            "body": f"Your Awesome App two factor authentication code is: {code}"
        }
    }, headers={
        "Authorization": "Api-Key XXXXX"
    })
...
             

This function submits a request to CronDock sms mini app for for sending a text message to the specified phone number. The CronDock sms mini app needs a phone number which you can provide through to_phone key and the message, which can be provided through body key in the request payload. CronDock sms basically sends the message to the phone number via SMS (Short Message Service) protocol. You can learn more about CronDock's sms API here.

Note that, in the headers you need to pass the API key you have received after signing up on CronDock . In the above example, you should replace XXXXX with your API key.

Next, let's look into the implementation of these three functions: save_code, check_code and delete_code

flask_app.py
...

def save_code(phone, code):
    conn = sqlite3.connect('example.db')
    curs = conn.cursor()
    curs.execute("insert into two_factor_authentication_codes (phone, code) values(?, ?) ",[phone, code])
    conn.commit()

def check_code(phone, code):
    conn = sqlite3.connect('example.db')
    curs = conn.cursor()
    curs.execute("SELECT * from two_factor_authentication_codes where phone = (?)",[phone])
    result = curs.fetchone()
    if result and result[1] == int(code): # 2fa confirmed
        return True
    else: # 2fa failed
        return False

def delete_code(phone):
    conn = sqlite3.connect('example.db')
    curs = conn.cursor()
    curs.execute("delete from two_factor_authentication_codes where phone = (?)",[phone])
    conn.commit()
...
              

The save_code function above basically saves the randomly generated code and the phone number we send that code to, in two_factor_authentication_codes table. The check_code function on the other hand, pulls the stored code for the phone number from the table and compares it with the code entered by the user. If the two codes matches, the function will return true, otherwise it will return false. Finally, the delete_code method removes any record associated with a phone number from the two_factor_authentication_codes table.

Next, I am going to explain the login function:

flask_app.py
...

@app.route("/login", methods=['GET','POST'])
def login():
    form = SignInForm()
    if form.validate_on_submit():
        user = load_user(form.username.data)
        if user and form.username.data == user.username and form.password.data == user.password:
            session["user_token"] = token = jwt.encode(
                {"username": user.username},
                app.config.get('SECRET_KEY'),
                algorithm='HS256'
            )
            code = randint(10000,99999)
            delete_code(user.phone)
            save_code(user.phone, code)
            send_2_factor_authentication(user.phone, code)
            return redirect("two_factor_auth")
        else:
            flash('Authentication failed')
    return render_template('login.html',title='Login', form=form)
...
              

The login() method, is being called when the app receives a POST or GET request. This method basically pulls the data from the form we defined in login.html, loads the user based on the provided username in the form, checks the password and if the password is correct it first encodes and caches the username in a session called user_token. Then, it generates a random 5 digit number as the 2FA code and stores it along with the user phone number in the two_factor_authentication_codes table. After that, it sends the code to the user's phone number via a text message by calling the send_2_factor_authentication function and finally redirects the user to the two_factor_auth.html page.

Next is the two_factor_auth function:

flask_app.py
...

 @app.route("/two_factor_auth", methods=['GET','POST'])
 def two_factor_auth():
     form = TwoFactorAuthForm()
     if form.validate_on_submit():
         code = form.code.data
         user_token = session["user_token"]
         decoded_data = jwt.decode(user_token, app.config.get('SECRET_KEY'), algorithms='HS256')
         user = load_user(decoded_data['username'])
         code_confirmed = check_code(user.phone, code)
         delete_code(user.phone)
         if user and code_confirmed:
             login_user(user)
             flash(f"Welcome {decoded_data['username']}")
         else:
             flash('Wrong code')
     return render_template('two_factor_auth.html',title='Two Factor Authentication', form=form)

...
              

This method is basically the backend for two_factor_auth.html page. When the user enters the code and pushes the submit button on the two_factor_auth.html page, this function first pulls the user_token value from the session and decodes it. Then, it pulls the username from the decoded data and loads the user data. Next, it checks whether the provided code is the correct code by calling the check_code function explained above. Finally, it deletes the code from the two_factor_authentication_codes table by calling delete_code function and then if the provided code is confirmed, it logs in the user and shows a welcome message. If the code is not correct, it shows a "'Wrong code" message to the user.

The last part is to define the host and the port we want to run the flask app on:

flask_app.py
...


if __name__ == "__main__":
   app.run(host='0.0.0.0',port=8080,threaded=True)
              

Now we can run the app in the terminal like this:

python flask_app.py
               

After that, we can open a web browser, go to "http://0.0.0.0:8080/login" to view the login page:

After providing the username and password (in this example username is set to "testuser" and password to "ABC123"), we should receive a text message on the phone number (in this example "18009998877") with a 5 digit code.

We then will be redirected to the two-factor authentication page:

After entering the code we should see the welcome message:

Beside two-factor-authentication, there are other things you can do with CronDock:

Using CronDock for sending messages

Using CronDock for converting webpages to PDFs

Using CronDock for converting webpages to images

Using CronDock for One Click two-factor authentication

Or programmatically create cron jobs that run on a frequent basis

Or jobs that run at a specific time.

Or even jobs with dependencies.

Please let me know if you have any question at support@crondock.com

I also have a good document which provides more detailed information on all the requests you can make to CronDock API.