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 numbertwo_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
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.