DEV Community

Cover image for Managing User Authentication and Sessions with Fauna and Flask
LordGhostX
LordGhostX

Posted on • Edited on

Managing User Authentication and Sessions with Fauna and Flask

Authored in connection with the Write With Fauna program.

Managing user authentication and sessions is a task you are bound to encounter when building web applications. You need to authenticate users on your platform and know if they have permission to access your servers. However, this task is not easy. This is why many third-party products are willing to help you get started.

In this article, you will be introduced to the concept of authentication, authorization, user identity, and session management. I will also discuss the benefits of handling user authentication with Fauna and then integrate Fauna’s built-in user authentication and session management capabilities into a Flask application.

Prerequisites

To follow and fully understand this tutorial, you will need to have:

  • Python 3.6 or a newer version.
  • A text editor.
  • An understanding of Fauna and Flask.

Introduction

Authentication vs. Authorization

Authentication (AuthN) validates that users are who they claim to be. This could be achieved by using one-time pins (OTP) or passwords when logging in to websites.

Authorization (AuthZ) is the process of giving a user permission to access a specific resource or function in a system. For example, only logged-in users can process their eCommerce shopping carts.

User Identity vs. User Session

User identity refers to a unique identifier for people who use an application. For example, "John Smith" and "John Doe" are entirely different people because their unique identifiers (full names) are not the same. In web applications, users are uniquely identifiable using information like usernames and email addresses.

A user session is the series of interactions and responses between a user and a web server, e.g., users logging-in to their dashboard and browsing around. It is also the information exchange between devices communicating with one another. Sessions help to manage specific user states, user identities, authorization, and many other interactions.

Fauna Serverless Database

Fauna is a flexible, developer-friendly, transactional cloud database delivered as a secure Data API. It provides built-in user authentication and session management capabilities, making their integration into web applications seamless.

Downloading the Demo Application

In this tutorial, you will be implementing user authentication and session management with Fauna into a dashboard with registration and login pages.

Clone the Demo App Repository

For the sake of convenience, I have written a Flask application with a Bootstrap user interface that we will use in this article. To get started, you will need to clone my repository and initialize the application like so:

git clone https://github.com/LordGhostX/fauna-auth-demo  
cd fauna-auth-demo/
Enter fullscreen mode Exit fullscreen mode

The user-interface directory contains only the demo’s user interface, while the fauna-integration directory contains the entire application (Fauna + Flask) that you will build in this tutorial.

Install the Demo Requirements

You need to install the external libraries required by the demo application before you can run it. In your terminal, type:

cd user-interface  
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Finally, run the application to make sure it’s working. In your terminal, type:

python3 app.py
Enter fullscreen mode Exit fullscreen mode

Setting Up the Fauna Database

Create the Fauna Database

You need to create the database for the demo application in Fauna’s dashboard. If you have not created an account on Fauna before now, create one on Fauna’s website.

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

Create the Database Collections

Now, you need to create a Fauna collection to store data collected in the database you just created. A collection is similar to SQL tables containing data with 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 then press the SAVE button.

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, move to the Indexes tab on the Fauna sidebar (left side of the screen), click the NEW INDEX button, provide the necessary information, and press the SAVE button.

Generate Database Security Key

Finally, you need to create a Security Key to connect your database to the demo application. Go to the Security tab on the Fauna sidebar (left side of the screen), click the NEW KEY button, provide the necessary information, and then press the SAVE button.

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

Integrating Fauna’s User Authentication

Now that you have set up the base demo application and your Fauna database let’s move to integrate Fauna’s built-in authentication feature as shown in the documentation.

Install Fauna’s Python Driver

You need to get the Python driver for Fauna, which extends Python programs’ functionality by providing functions that allow them to make queries to Fauna databases. It’s available on pip, and can be installed with a single command in your terminal.

In the terminal, type:

pip install faunadb
Enter fullscreen mode Exit fullscreen mode

Importing Fauna into Your Demo

You need to import the Fauna driver into your Flask application to use it in the current project. To do this, add the following block of code to the app.py file:

from functools import wraps  
from faunadb import query as q  
from faunadb.objects import Ref  
from faunadb.client import FaunaClient  
from faunadb.errors import BadRequest, Unauthorized
Enter fullscreen mode Exit fullscreen mode

You also have to initialize the Python driver client with your database Security Key so you can make queries to your database from your application. Add the following block of code in your app.py file right after initializing your Flask app.

app.config["SECRET_KEY"] = "APP_SECRET_KEY"  
client = FaunaClient(secret="YOUR-SECRET-HERE")
Enter fullscreen mode Exit fullscreen mode

Don’t forget to replace the secret keys' placeholders with the appropriate values for your application.

Creating User Profiles in Fauna

To create new user profiles that Fauna will help manage authentication and sessions, you need to create a new user document in your collection that contains the user’s email address and password.

Here is a sample Python code that demonstrates this functionality:

client.query(
    q.create(
        q.collection("users"), {
            "credentials": {"password": "secret password"},
            "data": {"email": "test@test.com"}
        }
    )
)
Enter fullscreen mode Exit fullscreen mode

The code above makes a Create query to your Fauna database. It creates a collection entry with the email address provided in the data parameter and password provided in the credentials parameter. Passing the credentials parameter is the key to creating user profiles in Fauna. Fauna can now use any entry with this parameter for user authentication.

You can integrate this into your application by updating the register route with the code below:

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

        try:
            result = client.query(
                q.create(
                    q.collection("users"), {
                        "credentials": {"password": password},
                        "data": {
                            "email": email,
                            "fullname": fullname
                        }
                    }
                )
            )
        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("login"))

    return render_template("register.html")
Enter fullscreen mode Exit fullscreen mode

Note: You can only create good user profiles by providing the email and password fields, and Fauna does not store credentials in plain text but uses a BCrypt hash of the password provided.

Authenticating Users with Fauna

To authenticate stored user profiles with Fauna, you need to provide the user’s email address and password, then use the Login function.

Here is a sample Python code that demonstrates this functionality:

client.query(
    q.login(
        q.match(q.index("users_by_email"), "test@test.com"), {
            "password": "secret password"}
    )
)
Enter fullscreen mode Exit fullscreen mode

The code above uses the Login built-in function of Fauna to authenticate a user profile’s reference with the password provided. The Ref of the account is retrieved using the index we created earlier for the users collection and the Match function in Fauna.

When you run the Login query, Fauna will provide you a secret token in the response that you use to make authorized requests for that user. Each time the query executes, you will get different values for the secret token. These values are to be stored after receiving them.

To integrate this block of code into our application, update the login route in the app.py file with the code below:

@app.route("/login/", methods=["GET", "POST"])
def login():
    if "user_secret" in session:
        return redirect(url_for("dashboard"))

    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("dashboard"))

    return render_template("login.html")
Enter fullscreen mode Exit fullscreen mode

Note: The image above shows the result of supplying invalid login credentials to the demo application.

Integrating Fauna’s Session Management

Validating User Sessions with Fauna

Validating user sessions means verifying that a particular session token is valid (authentic) before using it. Validation helps tokens expire based on conditions like reaching an expiry date or when a user logs out of a device.

To validate user sessions with Fauna, you need to make an authorized request with the built-in CurrentIdentity function to your database. The CurrentIdentity function returns information of the user associated with a session token.

Here is a sample Python code that demonstrates this functionality:

client = FaunaClient(secret="secret token")
client.query(
    q.current_identity()
)
Enter fullscreen mode Exit fullscreen mode

To integrate this block of code into our application, we need to create a Python wrapper that will perform the validation and protect routes that require the AuthN & AuthZ e.g. dashboard route. Add the following block of code to the app.py file:

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:
                session.clear()
                return redirect(url_for("login"))
        else:
            return redirect(url_for("login"))
        return f(result, *args, **kwargs)

    return decorated
Enter fullscreen mode Exit fullscreen mode

You also need to protect the dashboard route with the newly created decorator so only authorized visitors can access it. Update the dashboard route with the code below:

@app.route("/dashboard/", methods=["GET", "POST"])
@login_required
def dashboard(user):
Enter fullscreen mode Exit fullscreen mode

Fetching User Info with Sessions

To fetch users’ information with their sessions using Fauna, you need to use the same CurrentIdentity function for validating sessions earlier. The response contains the Fauna Ref of the user, which you can use with Get to retrieve the collection data.

Here is a sample Python code that demonstrates this functionality:

client.query(
    q.get(
        q.ref(q.collection("users"), "user ref ID")
    )
)
Enter fullscreen mode Exit fullscreen mode

To integrate this block of code into our application, update the dashboard route in the app.py file with the code below:

@app.route("/dashboard/", methods=["GET", "POST"])
@login_required
def dashboard(user):
    user_details = client.query(
        q.get(
            q.ref(q.collection("users"), user.id())
        )
    )

    return render_template("dashboard.html", user_details=user_details)
Enter fullscreen mode Exit fullscreen mode

You also need to update the dashboard template to render the data retrieved from your Fauna using Jinja. Update the welcome line in the dashboard.html file located in the templates directory with the code below:

<h5>Welcome {{ user_details.data.fullname }} 👋</h5>
Enter fullscreen mode Exit fullscreen mode

Deleting User Sessions with Fauna

Deleting user sessions means making specific user sessions unusable. This comes in handy when you want users to log out of their accounts.

To delete user sessions with Fauna, you need to make an authorized request with your database’s built-in Logout function. The Logout function deletes session tokens associated with a user profile.

Here is a sample Python code that demonstrates this functionality:

client = FaunaClient(secret="secret token")
result = client.query(
    q.logout(True)
)
Enter fullscreen mode Exit fullscreen mode

The Logout function takes a boolean parameter that tells Fauna to delete all session tokens associated with a user profile (True) or only the currently passed token (False).

To integrate this block of code into our application, update the logout route in the app.py file with the code below:

@app.route("/dashboard/logout/<string:logout_type>/")
@login_required
def logout(user, logout_type):
    if logout_type == "all":
        all_tokens = True
    else:
        all_tokens = False

    user_client = FaunaClient(secret=session["user_secret"])
    result = user_client.query(
        q.logout(all_tokens)
    )
    session.clear()

    return redirect(url_for("index"))
Enter fullscreen mode Exit fullscreen mode

Extra AuthN Functionalities

Changing User Password with Fauna

To change the password of stored user profiles with Fauna, you need to update your user collection, passing the request’s new credentials.

Here is a sample Python code that demonstrates this functionality:

client.query(
    q.update(
        q.ref(q.collection("users"), "user ref ID"), {
            "credentials": {"password": "new secret password"}
        }
    )
)
Enter fullscreen mode Exit fullscreen mode

The code above uses the Update built-in function of Fauna to change a user profile’s credentials to the new password provided.

To integrate this block of code into our application, update the dashboard route in the app.py file with the code below:

@app.route("/dashboard/", methods=["GET", "POST"])
@login_required
def dashboard(user):
    if request.method == "POST":
        old_password = request.form.get("old-password").strip()
        new_password = request.form.get("new-password").strip()

        try:
            result = client.query(
                q.identify(
                    q.ref(q.collection("users"), user.id()), old_password
                )
            )

            if not result:
                raise Exception()

            result = client.query(
                q.update(
                    q.ref(q.collection("users"), user.id()), {
                        "credentials": {"password": new_password}
                    }
                )
            )
        except Exception as e:
            flash(
                "You have supplied an invalid password!", "danger")
            return redirect(url_for("dashboard"))

        flash(
            "You have successfully changed your account password!", "success")
        return redirect(url_for("dashboard"))

    user_details = client.query(
        q.get(
            q.ref(q.collection("users"), user.id())
        )
    )

    return render_template("dashboard.html", user_details=user_details)
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, you learned the concept of authentication, authorization, user identity, session management, benefits of handling user authentication with Fauna, and integrated Fauna's built-in AuthN & AuthZ features into a Flask application.

The source code of the demo application is available on GitHub. I also created a Github Gist that shows Fauna's user identity capabilities using Python. If you have any questions, don't hesitate to contact me on Twitter: @LordGhostX

Top comments (0)