How to Implement Two-Factor Authentication with PyOTP and Google Authenticator in Your Flask App

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!

Leave a Comment

Your email address will not be published. Required fields are marked *