Two-factor authentication (2FA) is a security method that requires users to provide two pieces of information to verify their identity and access an online service. The first factor is usually a password, and the second factor is usually a one-time code generated by an app or sent via SMS. 2FA adds an extra layer of protection against hackers and identity theft, as it makes it harder for someone to access your account without your permission.
In this blog post, I will show you how to implement 2FA with PyOTP and Google Authenticator in your Flask app. PyOTP is a Python library that implements the Time-based One-time Password (TOTP) algorithm, which is used by Google Authenticator and other apps to generate and verify codes. Google Authenticator is a mobile app that generates codes for 2FA based on a secret key that you scan or enter manually.
To follow along, you will need:
- Python 3.6 or higher
- Flask 2.0 or higher
- PyOTP 2.6 or higher
- Google Authenticator app on your smartphon
Step 1: Create a Flask app
First, let’s create a simple Flask app that has a login page and a home page. The login page will ask for a username and a password, and the home page will display a welcome message. We will use Flask’s built-in session management to store the user’s login status.
Create a file called app.py
and paste the following code:
from flask import Flask, render_template, request, session, redirect, url_for
from flask.helpers import flash
app = Flask(__name__)
app.secret_key = "secret"
# A dummy user for demonstration purposes
user = {
"username": "admin",
"password": "password",
"secret": "JBSWY3DPEHPK3PXP" # A secret key for 2FA
}
@app.route("/")
def index():
# If the user is logged in, redirect to the home page
if session.get("logged_in"):
return redirect(url_for("home"))
# Otherwise, render the login page
return render_template("login.html")
@app.route("/login", methods=["POST"])
def login():
# Get the username and password from the form
username = request.form.get("username")
password = request.form.get("password")
# Check if they match the dummy user
if username == user["username"] and password == user["password"]:
# Set the session variable to indicate the user is logged in
session["logged_in"] = True
# Redirect to the home page
return redirect(url_for("home"))
# Otherwise, flash an error message and redirect to the login page
flash("Invalid username or password")
return redirect(url_for("index"))
@app.route("/home")
def home():
# If the user is not logged in, redirect to the login page
if not session.get("logged_in"):
return redirect(url_for("index"))
# Otherwise, render the home page
return render_template("home.html")
@app.route("/logout")
def logout():
# Clear the session variable and redirect to the login page
session.clear()
return redirect(url_for("index"))
if __name__ == "__main__":
app.run(debug=True)
Create two HTML templates called login.html
and home.html
in a folder called templates
. The login.html
template should look something like this:
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<form action="{{ url_for('login') }}" method="post">
<p>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</p>
<p>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</p>
<p>
<button type="submit">Login</button>
</p>
</form>
</body>
</html>
The home.html
template should look something like this:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Welcome, {{ user.username }}</h1>
<p>
<a href="{{ url_for('logout') }}">Logout</a>
</p>
</body>
</html>
Run the app with python app.py
and go to http://localhost:5000/
in your browser. You should see the login page, where you can enter the username admin
and the password password
to access the home page. You can also logout from the home page.
Step 2: Generate and verify codes with PyOTP
Next, let’s add 2FA to our app using PyOTP and Google Authenticator. We will use the secret key JBSWY3DPEHPK3PXP
that we assigned to the dummy user as the basis for generating and verifying codes. You can generate your own secret key using PyOTP’s random_base32()
function.
To use PyOTP, we need to install it with pip install pyotp
. Then, we need to import it in our app.py
file and create a TOTP
object with the secret key:
import pyotp
# ...
totp = pyotp.TOTP(user["secret"])
The TOTP
object has two methods that we will use: now()
and verify()
. The now()
method returns the current code as a string, and the verify()
method takes a code as an argument and returns True
or False
depending on whether the code is valid or not.
We will use the now()
method to generate a QR code that the user can scan with Google Authenticator to add the account to the app. We will use the verify()
method to check the code that the user enters in the app against the code generated by PyOTP.
To generate a QR code, we need to install another library called qrcode
with pip install qrcode
. Then, we need to import it in our app.py
file and create a function that takes a secret key and returns a QR code image:
import qrcode
# ...
def generate_qr(secret):
# Create a URL with the secret key and the account name
url = pyotp.totp.TOTP(secret).provisioning_uri(name="admin", issuer_name="Flask App")
# Create a QR code image from the URL
img = qrcode.make(url)
return img
The provisioning_uri()
method creates a URL that contains the secret key and the account name, and optionally the issuer name. The URL follows the format otpauth://totp/issuer_name:account_name?secret=secret_key&issuer=issuer_name
. The qrcode
library can create an image from this URL using the make()
function.
We will use this function to generate a QR code image and save it as qr.png
in a folder called static
. We will also create a route called /qr
that will return the image as a response:
# ...
@app.route("/qr")
def qr():
# Generate a QR code image with the user's secret key
img = generate_qr(user["secret"])
# Save the image as qr.png in the static folder
img.save("static/qr.png")
# Return the image as a response
return app.send_static_file("qr.png")
We will also modify the login.html
template to display the QR code image below the login form, with a link to the /qr
route:
<!-- ... -->
<form action="{{ url_for('login') }}" method="post">
<!-- ... -->
</form>
<p>
Scan this QR code with Google Authenticator to enable 2FA:
</p>
<p>
<img src="{{ url_for('qr') }}" alt="QR code">
</p>
<!-- ... -->
Now, if you run the app and go to the login page, you should see the QR code image below the login form. You can scan it with Google Authenticator to add the account to the app. You should see a six-digit code that changes every 30 seconds.
To verify the code, we need to add another input field to the login form, where the user can enter the code from the app. We will also modify the login()
function to check the code using the verify()
method of PyOTP:
<!-- ... -->
<form action="{{ url_for('login') }}" method="post">
<!-- ... -->
<p>
<label for="code">Code:</label>
<input type="text" id="code" name="code" required>
</p>
<!-- ... -->
</form>
<!-- ... -->
# ...
@app.route("/login", methods=["POST"])
def login():
# Get the username, password and code from the form
username = request.form.get("username")
password = request.form.get("password")
code = request.form.get("code")
# Check if they match the dummy user and the code is valid
if username == user[“username”] and password == user[“password”] and totp.verify(code):
# Set the session variable to indicate the user is logged in
session[“logged_in”] = True
# Redirect to the home page
return redirect(url_for(“home”))
# Otherwise, flash an error message and redirect to the login page
flash(“Invalid username, password or code”)
return redirect(url_for(“index”))
That’s it! You have successfully implemented 2FA with PyOTP and Google Authenticator in your Flask app. You can test it by running the app and logging in with the username `admin`, the password `password` and the code from the app. You should be able to access the home page only if the code is correct. You can also try to enter a wrong code or wait for the code to expire and see the error message.
Conclusion:
In this blog post, you learned how to implement 2FA with PyOTP and Google Authenticator in your Flask app. You learned how to generate and verify codes using the TOTP algorithm, how to create a QR code image that the user can scan with the app, and how to add an extra input field to the login form to ask for the code. You also learned how to use Flask’s session management to store the user’s login status.
2FA is a simple and effective way to enhance the security of your online service and protect your users from unauthorized access. You can use PyOTP and Google Authenticator to easily add 2FA to your Flask app or any other Python project that requires authentication. I hope you found this blog post useful and interesting. Thank you for reading!