Implementing Two-factor Authentication with Flask, PyOTP, and Fauna
Tags: fauna, python, security, serverless
Published: 2021-04-23 14:35:23

_Written in connection with the [Write with Fauna](https://fauna.com/blog/write-with-fauna) program._

This article focuses on implementing two-factor authentication in a web application built with Python and Flask using Fauna’s built-in authentication features and PyOTP for generating and verifying one-time passwords.

[Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_Auth_SEsenyi) is a flexible, developer-friendly, transactional cloud database delivered as a secure Data API that provides two interfaces: GraphQL and also the Fauna Query Language (FQL), providing functionality to store collections, indexes, and other databases (multi-tenancy). It also offers built-in user authentication and session management features. To learn more about Fauna, [visit the official documentation](https://docs.fauna.com/fauna/current/start/index.html).

This article includes a step-by-step guide on integrating Fauna’s authentication features into a Flask web application. You will also add another layer of security to user authentication and authenticate users with multiple factors using PyOTP. To find out more about PyOTP, [visit the official documentation](https://pyauth.github.io/pyotp/).

# Setting Up the Fauna Database

## Create the Fauna Database

You need to create the database for the web application in Fauna’s dashboard. If you have not created an account on Fauna before now, create one on [Fauna’s website](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_Auth_SEsenyi).

In the dashboard, click on the `NEW DATABASE` button, provide a name for your database then press the `SAVE` button.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ugrnwcg326xc0cobtx0d.png "creating database")

## Create the Database Collections

You need to create a Fauna collection to store data collected in the database you just created. A collection is analogous to SQL tables containing similar characteristics, e.g., a user collection with information about users in the database.

To create a collection, navigate to the `Collections` tab on the Fauna sidebar (left side of the screen), click on the `NEW COLLECTION` button, provide a name for the collection you want to create, and press the `SAVE` button.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcosgw75bwfj2mcvj5rv.png "creating collections")

## Create the Collection Indexes

You need to create an index for the collection of the database. A Fauna index allows you to browse through data stored in a database collection based on specific attributes.

To create an index, head to the `Indexes` tab on the Fauna sidebar (left side of the screen), click the `NEW INDEX` button, provide the required information, and press the `SAVE` button.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ovdyex0lg1n6sotfzj5p.png "creating indexes")

## Generate Database Security Key

Now, you need to create a `security key` to connect your database to any application using any of [Fauna’s drivers](https://docs.fauna.com/fauna/current/drivers/). Head to the `Security` tab on the Fauna sidebar (left side of the screen), click the `NEW KEY` button, provide the required information, and then press the `SAVE` button.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rjyqcly4uecbilj4l43v.png "creating fauna security key")

Once you have done this, Fauna will display your `Secret Key`. You should copy the key as soon as Fauna generates it and store it somewhere secure and easily retrievable because Fauna will only show this once.

## Integrating Fauna into Python

Next, you need to get the Python driver for Fauna. You can install it from [PIP](https://realpython.com/what-is-pip/) with one line in your terminal.

```
pip install faunadb
```

After this, you should run the sample code provided in the [Fauna Python driver documentation](https://docs.fauna.com/fauna/current/drivers/python.html).

```python
from faunadb import query as q  
from faunadb.objects import Ref  
from faunadb.client import FaunaClient   

client = FaunaClient(secret="your-secret-here")  

indexes = client.query(q.paginate(q.indexes()))  

print(indexes)
```

The code above shows how the Fauna Python driver connects to a database with its `Secret Key` and prints the indexes associated with it. The result from running this code should look like the image below.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/01gniyxiqxievoizvjby.png)

# One-factor Authentication with Fauna

Now that you have successfully set up your Fauna database and integrated a Python script with it, let's get started with the web app and implementing Fauna’s built-in authentication. The app backend will be built with [Flask](https://flask.palletsprojects.com/en/1.1.x/), while the user interface will be built with [Flask-Bootstrap](https://pypi.org/project/Flask-Bootstrap4/).

You will be designing four (4) web pages for the app. They are:

* Index/Landing Page
* Registration Page
* Login Page
* Authentication Success Page

## Step 1: Installing the Project’s Requirements

We need to install the Flask library and Flask-Bootstrap. We will do that the same way we installed Fauna earlier using PIP. In your terminal, type:

```
pip install Flask Flask-Bootstrap4
```

## Step 2: Setting Up the Flask Server

Create a file with the name `app.py` and save the subsequent code in it:

```python
from flask import *
from flask_bootstrap import Bootstrap

app = Flask(__name__)
Bootstrap(app)


@app.route("/")
def index():
    return "Hello World!"


if __name__ == "__main__":
    app.run(debug=True)
```

When you run the `app.py` file and open your browser, you will get a response similar to the image below:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t8a12zr54ds4oftm1xb8.png)

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jxj5ablohqpg7kqculba.png)

## Step 3: Designing the Landing Page

The landing page is where your users see your application’s information with navigation links to various routes within the app.

First, create a folder named `templates` within the same folder as your `app.py`. Flask uses a `templates` folder to store its HTML files that the server renders within the application. Your project folder should resemble the image below:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/96dwv4rqelsa6im0y8vg.png)

Create another file named `index.html` which will be stored in the `templates` folder and save the following code in it:

```html
{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
      </div>
    </div>
    <div class="col-lg-9 text-center">
      <h5>This is a Python + Flask demo application implementing two-factor authentication using Fauna's built-in authentication and PyOTP.</h5>
    </div>
    <div class="col-lg-5 text-center">
      <a href="{{ url_for('register') }}" class="btn btn-success m-3">GET STARTED</a>
    </div>
  </div>
</div>
{% endblock %}
```

You will also need to update the `app.py` file to implement the `register` route referenced in the landing page. Update the `app.py` file with the code below:

```python
@app.route("/")
def index():
    return render_template("index.html")


@app.route("/register/")
def register():
    return "Register Page!"


@app.route("/login/")
def login():
    return "Login Page!"
```

When you run the `app.py` file, you will get a response just like the image below in your browser:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y9loz3z6p9kbkyzw7x2q.png)

## Step 4: Building the Registration Page

The registration page is where your users create new accounts on the application.

> To create new user profiles that Fauna will help manage authentication, you need to create a new user document in your collection containing the user’s email address and password within the credentials parameter. Here is a sample Python code that demonstrates this functionality:

```python
client.query(
    q.create(
        q.collection("users"), {
            "credentials": {"password": "secret password"},
            "data": {"email": "test@test.com"}
        }
    )
)
```

You need to update the `register` route within the `app.py` file to render the registration page template; update the `register` route with the code below:

```python
@app.route("/register/")
def register():
    return render_template("register.html")
```

Create another file named `register.html` that will be stored in the `templates` folder and save the following code in it:

```python
{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Create an account to get started!</h5>
      </div>
    </div>

    <div class="col-lg-6">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="email">Email Address</label>
          <input type="email" class="form-control" id="email" name="email" placeholder="Enter Email Address" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" id="password" name="password" placeholder="Choose Password" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Create Account</button>
          <a href="{{ url_for('login') }}"><small class="form-text text-success">Already have an account? Login</small></a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}
```

When you run the `app.py` file, your register page should resemble the image below:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pcnmosfj5l5mrjrmbkcl.png)

You need to import the required libraries required by the application. Update the `app.py` file imports with the code below:

```python
from faunadb import query as q  
from faunadb.objects import Ref  
from faunadb.client import FaunaClient  
from faunadb.errors import BadRequest, Unauthorized
```

You also need to configure the application configurations (Fauna secret key, app secret key). Update the `app.py` file with the code below:

```python
app.config["SECRET_KEY"] = "APP_SECRET_KEY"  
client = FaunaClient(secret="FAUNA_SECRET_KEY")
```

Next, you’ll update the `register` route to record new users creation details. Update the `app.py` file with the code below:

```python
@app.route("/register/", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        email = request.form.get("email").strip().lower()
        password = request.form.get("password")

        try:
            result = client.query(
                q.create(
                    q.collection("users"), {
                        "credentials": {"password": password},
                        "data": {"email": email}
                    }
                )
            )
        except BadRequest as e:
            flash("The account you are trying to create already exists!", "danger")
            return redirect(url_for("register"))

        flash(
            "You have successfully created your account, you can proceed to login!", "success")
        return redirect(url_for("register"))

    return render_template("register.html")
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3wa51lwpfx1t1xmkfbc.png)

## Step 5: Building the Authentication Page

The authentication page is where users provide their credentials to the application and grant authorization.

> To authenticate stored user profiles with Fauna, you need to provide the user’s email address and password, then use the [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function. Here is a sample Python code that demonstrates this functionality:

```python
client.query(
    q.login(
        q.match(q.index("users_by_email"), "test@test.com"), {
            "password": "secret password"}
    )
)
```

Create a file named `login.html` that will be stored within the `templates` folder and save the following code in it:

```html
{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Login to access your account!</h5>
      </div>
    </div>

    <div class="col-lg-6">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="email">Email Address</label>
          <input type="email" class="form-control" id="email" name="email" placeholder="Enter Email Address" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" id="password" name="password" placeholder="Enter Account Password" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Access Account</button>
          <a href="{{ url_for('register') }}"><small class="form-text text-success">Don't have an account? Register</small></a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}
```

Next, you will update the `login` route to integrate Fauna’s built-in authentication into our application. Update the `app.py` file with the code below:

```python
@app.route("/login/", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        email = request.form.get("email").strip().lower()
        password = request.form.get("password")

        try:
            result = client.query(
                q.login(
                    q.match(q.index("users_by_email"), email), {
                        "password": password}
                )
            )
        except BadRequest as e:
            flash(
                "You have supplied invalid login credentials, please try again!", "danger")
            return redirect(url_for("login"))

        session["user_secret"] = result["secret"]
        return redirect(url_for("auth_success"))

    return render_template("login.html")


@app.route("/auth-success/")
def auth_success():
    return "<h1>Successfully authenticated account using Fauna!</h1>"
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9zbt3p5kes6tnocjaurv.png)

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9m1vluvwsml35x7xklpy.png)

## Step 6: Implementing Authorization Rules in Flask

You would want to make the `auth_success` route only accessible by users who have successfully authenticated themselves. Anyone can access the page and obtain the required response, but it shouldn’t be that way.

You need to import the `functools` library into the application to create custom wrappers for its routes. Update the `app.py` file imports with the code below:

```python
from functools import wraps
```

Next, you will update the `app.py` file with the code below to define a custom wrapper for ensuring a user is authenticated:

```python
def login_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if "user_secret" in session:
            try:
                user_client = FaunaClient(secret=session["user_secret"])
                result = user_client.query(
                    q.current_identity()
                )
            except Unauthorized as e:
                flash("Your login session has expired, please login again!", "danger")
                return redirect(url_for("login"))
        else:
            flash("You need to be logged in before you can access here!", "danger")
            return redirect(url_for("login"))
        return f(*args, **kwargs)

    return decorated
```

Now you’ve defined the custom wrapper. You need to implement it in routes that need it. Update your `auth_success` route with the code below:

```python
@app.route("/auth-success/")
@login_required
def auth_success():
    return "<h1>Successfully authenticated account using Fauna!</h1>"
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/drewu5mrbfwpjb44y5qs.png)

# Two-factor Authentication with PyOTP

After setting up authentication with Fauna, you want to integrate two-factor authentication. In this tutorial, you will be using [PyOTP](https://pyauth.github.io/pyotp/), a Python library that generates and verifies one-time passwords.

You will be designing two (2) additional web pages for the app. They are:

* 2FA Enrollment Page
* 2FA Verification Page

## Step 1: Installing the PyOTP Library

In your terminal, type:

```
pip install pyotp
```

## Step 2: Updating Old Authentication Flow

You need to update the previous authentication flow to accommodate the new two-factor authentication update. You will generate a 2FA seed token when registering a new user account and redirecting them to verify their OTP before their dashboard.

First, import the PyOTP library into the application’s code. Update the `app.py` file imports with the code below:

```python
import pyotp
```

Next, update the Fauna query in the `register` route within the `app.py` file with the code below to generate a 2FA seed token alongside the user profile:

```python
try:
    result = client.query(
        q.create(
            q.collection("users"), {
                "credentials": {"password": password},
                "data": {
                    "email": email,
                    "auth_enrolled": False,
                    "auth_secret": pyotp.random_base32()
                }
            }
        )
    )
except BadRequest as e:
    flash("The account you are trying to create already exists!", "danger")
    return redirect(url_for("register"))
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s95v2zf5t8lhmy9bhsyz.png)

> The `auth_enrolled` parameter stores if the current user is enrolled or not, while `auth_secret` generates and verifies 2FA tokens.

You will also be updating the `login` route to redirect the user to the 2FA verification page instead of the previous `auth_success` route.

```python
session["user_secret"] = result["secret"]  
session["verified_2fa"] = False  
return redirect(url_for("verify_2fa"))
```

You will also need to update the `app.py` file to implement the `verify_2fa` route the app will redirect the user to after successful one-factor authentication. Update the `app.py` file with the code below:

```python
@app.route("/2fa/verify/")
@login_required
def verify_2fa():
    return "Verify 2FA Page!"
```

Next, you will implement a few authorization rules in the app, like preventing users who have not enrolled 2FA from accessing the `verify_2fa` route. To do this, you need a custom wrapper that handles this process. Update the `app.py` file with the code below:

```python
def get_user_details(user_secret):
    user_client = FaunaClient(secret=user_secret)
    user = user_client.query(
        q.current_identity()
    )
    user_details = client.query(
        q.get(
            q.ref(q.collection("users"), user.id())
        )
    )
    return user_details


def auth_enrolled(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        user_details = get_user_details(session["user_secret"])
        if not user_details["data"]["auth_enrolled"]:
            return redirect(url_for("enroll_2fa"))
        return f(*args, **kwargs)

    return decorated


def auth_not_enrolled(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        user_details = get_user_details(session["user_secret"])
        if user_details["data"]["auth_enrolled"]:
            return redirect(url_for("verify_2fa"))
        return f(*args, **kwargs)

    return decorated
```

Now you’ve defined the custom wrapper. You need to implement it in routes that need it. Update your `verify_2fa` route with the code below:

```python
@app.route("/2fa/verify/")
@login_required
@auth_enrolled
def verify_2fa():
    return "Verify 2FA Page!"


@app.route("/2fa/enroll/")
@login_required
@auth_not_enrolled
def enroll_2fa():
    return "Enroll 2FA Page!"
```

## Step 3: Building the 2FA Enrollment Page

The 2FA enrollment page is where users are provided with their 2FA secret tokens to be registered on their [authenticator app](https://en.wikipedia.org/wiki/Google_Authenticator) and tested on the web application.

You need to update the `enroll_2fa` route within the `app.py` file to render the enrollment page template; update the `enroll_2fa` route with the code below:

```python
@app.route("/2fa/enroll/")
@login_required
@auth_not_enrolled
def enroll_2fa():
    user_details = get_user_details(session["user_secret"])
    secret_key = user_details["data"]["auth_secret"]

    return render_template("enroll_2fa.html", secret=secret_key)
```

Create another file named `enroll_2fa.html` that will be stored in the `templates` folder and save the following code in it:

```python
{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Set Up TOTP 2FA!</h5>
      </div>
    </div>

    <div class="col-lg-5">
      <form>
        <div>
          <h5>Instructions!</h5>
          <ul>
            <li>Download <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US" target="_blank">Google Authenticator</a> on your mobile.</li>
            <li>Create a new profile with the <strong>SECRET KEY</strong> below.</li>
            <li>Submit the generated OTP in the form for authentication.</li>
          </ul>
        </div>
        <div class="form-group">
          <label for="secret">SECRET KEY</label>
          <input type="text" class="form-control" id="secret" value="{{ secret }}" readonly>
        </div>
        <div class="text-center">
          <button type="button" class="btn btn-success mb-3" onclick="copySecret()">Copy Secret Key</button>
        </div>
      </form>
    </div>
    <div class="col-lg-7 m-auto">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="otp">Enter Generated OTP</label>
          <input type="number" class="form-control" id="otp" name="otp" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Test 2FA Authentication</button>
        </div>
      </form>
    </div>
  </div>
</div>

<script>
  function copySecret() {
    /* Get the text field */
    var copyText = document.getElementById("secret");

    /* Select the text field */
    copyText.select();
    copyText.setSelectionRange(0, 99999); /*For mobile devices*/

    /* Copy the text inside the text field */
    document.execCommand("copy");

    alert("Successfully copied Secret Key!");
  }
</script>
{% endblock %}
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ei3ultvs8qbgnwciu23.png)

Next, you will update the `enroll_2fa` route to verify the submitted 2FA token and mark the user account as enrolled if the verification was successful. Update the `app.py` file with the code below:

```python
@app.route("/2fa/enroll/", methods=["GET", "POST"])
@login_required
@auth_not_enrolled
def enroll_2fa():
    user_details = get_user_details(session["user_secret"])
    secret_key = user_details["data"]["auth_secret"]

    if request.method == "POST":
        otp = int(request.form.get("otp"))

        if pyotp.TOTP(secret_key).verify(otp):
            user_details["data"]["auth_enrolled"] = True
            client.query(
                q.update(
                    q.ref(q.collection("users"), user_details["ref"].id()), {
                        "data": user_details["data"]
                    }
                )
            )
            flash("You have successfully enrolled 2FA for your profile, please authenticate yourself once more!", "success")
            return redirect(url_for("verify_2fa"))
        else:
            flash("The OTP provided is invalid, it has either expired or was generated using a wrong SECRET!", "danger")
            return redirect(url_for("enroll_2fa"))

    return render_template("enroll_2fa.html", secret=secret_key)
```

## Step 4: Building the 2FA Verification Page

The 2FA verification page is where users verify their current login session right after authenticating themselves with their password.

You need to update the `verify_2fa` route within the `app.py` file to render the verification page template; update the `verify_2fa` route with the code below:

```python
@app.route("/2fa/verify/")
@login_required
def verify_2fa():
    return render_template("verify_2fa.html")
```

Create another file named `verify_2fa.html` that will be stored in the `templates` folder and save the following code in it:

```python
{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Set Up TOTP 2FA!</h5>
      </div>
    </div>

    <div class="col-lg-5">
      <h5>Instructions!</h5>
      <ul>
        <li>Download <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US" target="_blank">Google Authenticator</a> on your mobile.</li>
        <li>Enroll your account.</li>
        <li>Submit the generated OTP in the form for authentication.</li>
      </ul>
    </div>
    <div class="col-lg-7">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="otp">Enter Generated OTP</label>
          <input type="number" class="form-control" id="otp" name="otp" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Authenticate Account</button>
          <a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}
```

You will also need to update the `app.py` file to implement the `logout` route referenced in the 2FA verification page. Update the `app.py` file with the code below:

```python
@app.route("/logout/")
def logout():
    user_client = FaunaClient(secret=session["user_secret"])
    result = user_client.query(
        q.logout(True)
    )
    session.clear()
    return redirect(url_for("index"))
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2h3560bz0azjsamcj4o.png)

Next, you will update the `verify_2fa` route to verify the submitted 2FA token and complete the two-factor authentication of the user account. Update the `app.py` file with the code below:

```python
@app.route("/2fa/verify/", methods=["GET", "POST"])
@login_required
@auth_enrolled
def verify_2fa():
    if request.method == "POST":
        otp = int(request.form.get("otp"))

        user_details = get_user_details(session["user_secret"])
        secret_key = user_details["data"]["auth_secret"]

        if pyotp.TOTP(secret_key).verify(otp):
            session["verify_2fa"] = True
            return redirect(url_for("auth_success"))
        else:
            flash("The OTP provided is invalid, it has either expired or was generated using a wrong SECRET!", "danger")
            return redirect(url_for("verify_2fa"))
    return render_template("verify_2fa.html")
```

## Step 5: Implementing Authorization Rules in Flask

You would want to make the `auth_success` route only accessible by users who have successfully authenticated themselves with their password and 2FA.

To do this, you need a custom wrapper that handles this process. Update the `app.py` file with the code below:

```python
def auth_verified(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not session.get("verify_2fa"):
            return redirect(url_for("verify_2fa"))
        return f(*args, **kwargs)

    return decorated
```

You have now defined the custom wrapper. You need to implement it in routes that need it. Update your `auth_success` route with the code below:

```python
@app.route("/auth-success/")
@login_required
@auth_verified
def auth_success():
    return "<h1>Successfully authenticated account using Fauna and PyOTP!</h1>"
```

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fh6a7i54zugye0fk0me.png)

# Conclusion

In this article, you built a web application with Python and Flask, then implemented two-factor authentication using Fauna’s built-in user identity and session management capabilities and PyOTP for generating and verifying one-time passwords.

Looking to build the demo application further, look at sample code, or improve its functionality? Visit the [GitHub Repo](https://github.com/LordGhostX/fauna-2fa-demo). I also created a [Github Gist](https://gist.github.com/LordGhostX/9d70357f9803904506939ab6f4bb3a9a) that shows Fauna's user identity and session management capabilities using Python. If you have any questions, don't hesitate to contact me on Twitter: [@LordGhostX](https://twitter.com/LordGhostX).