Exploring JWT Authentication in Django : Part 1 - Understanding the Basics

Posted on: 25 April 2024 | Last updated: 25 April 2024 | Read: 9 min read | In: Django, Python

Description

In this blog series, we explore implementing JWT authentication in Django. In Part 1, we set up the project and make it production-ready. We follow a short and simple approach to create a separate environment for the Python project. Then, we install essential packages and bootstrap the Django project. Additionally, we set up a PostgreSQL database and use environment variables to avoid hard-coded values. Part 2 will delve into implementing JWT authentication. Stay tuned!

Full blog

Exploring JWT Authentication in Django : Part 1 - Understanding the Basics

Hello World 👋, I am Dakshesh Jain, and in this blog series, we will explore JWT Authentication in Django. In this first part, we will focus on setting up the project, understanding the basics, and changing the User model.

1. Setting up the project

For purpose of this blog series, we will keep the project setup nice and simple. I won't use cookie-cutters, poetry, or any other fancy tools. Although, I highly recommend looking into them if you are building a production application.

1.1. Setting up the environment

It's a good practice to have a separate environment for each Python project to ensure that any update to dependencies doesn't break our existing projects. Again, you can setup virtual environment however you like but I will keep it simple and will use the following commands:

python3 -m venv env
source env/bin/activate

1.2. Installing packages

Let's install a few packages that we will need. Run the following command to install the required packages. I encourage you to go through their documentation, but I'll try to cover as much as I can in this blog series.

pip install django djangorestframework djangorestframework-simplejwt django-cors-headers psycopg2 python-dotenv dj-database-url google-auth

1.3. Bootstrapping Django project

Let's bootstrap our Django project using the following command:

django-admin startproject <project_name> . # we will call our project confetti

1.4. Spinning up PostgreSQL database

If you wish to use a different database, feel free to do so. I will use PostgreSQL for this project. PostgreSQl is recommended by many in the Django community for various reasons(such as performance, scalability, transaction support, etc.).

This step can change depending on how you want to set up your database. I typically use this command to create a database in my computer:

createdb confetti # confetti is the name of the database/project

If this command doesn't work for you, and you have PostgreSQL installed with pgAdmin, you can use the GUI to create a database. Simply follow these steps:

  1. Open pgAdmin
  2. Click on Servers Dropdown -> PostgreSQL
  3. Right-click on Databases -> Click on Create -> Database
  4. A modal will appear; add the Database name.
  5. Click on Save

Once you have your database running, you can make your database url as such:

DATABASE_URL=postgres://<username>:<password>@localhost:<port>/confetti
# replace <username>, <password>, and <port> with your PostgreSQL username, password, and port respectively.

Again, confetti is the name of the database/project.

2. Creating and using virtual environments

2.1. Using environment variables

Create a .env file in the root of your project and populate the following variables:

SECRET_KEY = "<- secure key ->"
DEBUG = True
DATABASE_URL="postgres://postgres:password@localhost:5432/confetti"

2.2. Update settings.py

Since we are using python-dotenv to manage our environment variables, we need to make some changes to our settings.py file:

  1. Import dotenv and load the dotenv. Below is how the code might look:
import os
import dotenv

# Load environment variables from .env file
dotenv_file = os.path.join(BASE_DIR, ".env")
if os.path.isfile(dotenv_file):
    dotenv.load_dotenv(dotenv_file)
  1. Use the environment variables instead of hard-coded values:
SECRET_KEY = os.environ['SECRET_KEY']
DEBUG = os.environ.get('DEBUG', False)
  1. Use the PostgreSQL database we created. For that, you will have to do two things:

    1. Import dj_database_url in the settings.py file:
    2. Set the default database using the following code
    DATABASES = {
        'default': dj_database_url.config(
            default=os.environ.get('DATABASE_URL'),
            conn_max_age=600,
            conn_health_checks=True,
        )
    }

3. Understanding the User Model

Let's go over some member functions and fields to get an understanding of Django's User Model and how to customize it. The primary fields in AbstractUser are:

  1. username: All usernames must be unique, and this is used by Django for user login.
  2. first_name & last_name: These fields store the user's first and last names.
  3. email: Users can have duplicate emails, and it can be left blank.
  4. is_staff & is_superuser: These fields indicate whether a user is an admin or staff.
  5. EMAIL_FIELD: This specifies the field used as the email.
  6. USERNAME_FIELD: This field specifies which field will be used for authentication. By default, it is set to username but you can change it to be email.
  7. REQUIRED_FIELDS: This field lists the required fields, helpful when creating a superuser with python3 manage.py createsuperuser.
  8. Other fields like password, id, etc., are not discussed in detail as they are not as important.

Note: if you want to explore Django's User Model in detail or want to learn more about extending the user model, you can refer to the official documentation here. Alternatively, you can also go through the source code of Django.

If you check the AbstractUser class, you will see that it inherits from AbstractBaseUser and PermissionsMixin. The AbstractBaseUser class is a minimal class for implementing a user model. It contains logic to hash and check passwords, and have a few more fields that are required for a user model. The PermissionsMixin class adds the fields and methods necessary to support Django's permission model.

Now, depending on the project requirements, you can do the following:

  1. Just use the Django default User model.
  2. Extend AbstractUser and add more fields as required or just change the USERNAME_FIELD.
  3. Lastly, if you want more control over the user model, you can extend AbstractBaseUser and PermissionsMixin. And define your own fields and methods.

In this blog series, we will go with the third option. We will extend AbstractBaseUser and PermissionsMixin to have more control over the user model. Another reason is that it will cover more ground and give us a better understanding of how Django's User model works.

4. Crafting our User Model

Let's create a new app called users to handle all user-related functionalities. Run the following command to create a new app:

python manage.py startapp users

Now, let's tell Django about our new app. Add the app to the INSTALLED_APPS list in the settings.py file:

INSTALLED_APPS = [
    ...
    'users.apps.UsersConfig',
]

Finally, let's create our custom user model. Inside users/models.py, add the following code:

from django.contrib.auth.models import (
    AbstractBaseUser,
    BaseUserManager,
    PermissionsMixin,
)
from django.db import models


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        """Creates and saves a new user"""
        if not email:
            raise ValueError("Users must have an email address")

        user = self.model(email=self.normalize_email(email), **extra_fields)
        user.set_password(
            password
        )  # encrypts the password; never do user.password = password because it will be stored as plain text
        user.save(using=self._db)  # using=self._db is for supporting multiple databases
        return user

    def create_superuser(self, email, password):
        """Creates and saves a new superuser"""
        user = self.create_user(email, password)

        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    """Custom user model that supports using email instead of username"""

    email = models.EmailField(
        max_length=255, unique=True
    )  # unique=True means that the email must be unique in the database
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)

    # add more fields & member function if you need for your use-case

    objects = UserManager()

    USERNAME_FIELD = "email"  # this is the field used to log in

    def __str__(self):
        """Return string representation of user"""
        return f"{self.email}"

Most of the inspiration for above code comes from the Django documentation and by browsing the Django source code.

Before we can run the migrations, we have to update the AUTH_USER_MODEL in settings.py to point to your custom user model:

AUTH_USER_MODEL = 'users.User'

Finally, run the following commands to apply the changes:

python3 manage.py makemigrations
python3 manage.py migrate

Assuming everything went well, you can create a superuser by running the following command:

python3 manage.py createsuperuser

Before wrapping up this blog, I would like to mention that the code provided is a simplified version, and for a production environment, you should consider additional security and validation measures, as well as any specific requirements for your application.

That's it for the first part of our blog. In the next part, we'll dive into implementing JWT authentication in Django.

Stay tuned for Part 2!