FullStack JWT Authentication and Authorization System with Django and SvelteKit

sirneij

John Owolabi Idogun

Posted on February 7, 2022

FullStack JWT Authentication and Authorization System with Django and SvelteKit

Introduction

This is the first of a series of articles that will give a work-through of how to build a secure, robust, and reliable Authentication and Authorization system using modern web technologies viz: Django, Django REST Framework, JWT, and SvelteKit. It also demonstrates the new paradigm called #transitionalapps, a fusion of #SPA and #MPA, arguably propounded by @richharris in this talk.

Motivation

A while ago, I built a data-intensive application that collects, analyzes, and visualizes data using Django, Plotly, and Django templating language. However, an upgrade was recently requested which made me tend to re-develop the application from the ground up. A pivotal aspect of the app is Authentication and Authorization system since the data are confidential and only authorized personnel should be allowed access. I thought of making the architecture strictly client-server while maintaining Django at the backend. The major battle I had was choosing a suitable JavaScript frontend framework/library. I had had some upleasant attempts in learning React in the past but a fairly pleasant one with Vue. I thought of Svelte and/or it's "extension", SvelteKit with SSR. I had no experience working with it so I decided to learn it. This Dev Ed's youtube tutorial sold me all out! I decided to write about my experiences and challenges along the way and how I edged them since resources on SvelteKit are relatively scarce compared to React, Vue, and Angular but surprisingly faster without compromising SEO. On the backend, I was tired of using cookies and storing them in the browser to track users so I opted for JSON Web Tokens (JWT). Though I initially wrote the JWT authentication backend from scratch, I eventually settled for Django REST Framework Simple JWT.

Tech Stack

As briefly pointed out in the introduction, we'll be using:

Source code

The overall source code for this project can be accessed here:

GitHub logo Sirneij / django_svelte_jwt_auth

A robust and secure Authentication and Authorization System built with Django and SvelteKit

django_svelte_jwt_auth

This is the codebase that follows the series of tutorials on building a FullStack JWT Authentication and Authorization System with Django and SvelteKit.

This project was deployed on heroku (backend) and vercel (frontend) and its live version can be accessed here.

To run this application locally, you need to run both the backend and frontend projects. While the latter has some instructions already for spinning it up, the former can be spinned up following the instructions below.

Run locally

To run locally

  • Clone this repo:

     git clone https://github.com/Sirneij/django_svelte_jwt_auth.git
    
  • Change directory into the backend folder:

     cd backend
    
  • Create a virtual environment:

     pipenv shell
    

    You might opt for other dependencies management tools such as virtualenv, poetry, or venv. It's up to you.

  • Install the dependencies:

    pipenv install
    
  • Make migrations and migrate the database:

     python manage.py makemigrations
     python manage.py migrate
    
  • Finally, run the application:

     python manage.py runserver
    

Live version

This project was deployed on heroku (backend) and vercel (frontend) and its live version can be accessed here.

Assumption

It is assumed you are familiar with Python 3.9 and its type checking features, and Django. Also, you should know the basics of TypeScript as we'll be using that with SvelteKit.

Initial project setup

Currently, the structure of the project is as follows (exluding the node_modules folder):

├── backend
│   ├── backend
│   │   ├── asgi.py
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
├── frontend
│   ├── package.json
│   ├── package-lock.json
│   ├── README.md
│   ├── src
│   │   ├── app.d.ts
│   │   ├── app.html
│   │   └── routes
│   │       └── index.svelte
│   ├── static
│   │   └── favicon.png
│   ├── svelte.config.js
│   └── tsconfig.json
├── Pipfile
└── Pipfile.lock
Enter fullscreen mode Exit fullscreen mode

This intitial setup can be grabed here. If you would like to rather incept from the groundup, create a folder with your preferred name. In my case, I chose django_svelte_jwt_auth. Change your directory into it. Fire up a virtual environment, install the dependencies, and also initialize the sveltekit app. The processes are summarized in the commands below:

sirneij@pop-os ~/D/P/Tutorials> mkdir django_svelte_jwt_auth && cd django_svelte_jwt_auth  #create directory and change directory into it

sirneij@pop-os ~/D/P/T/django_svelte_jwt_auth> pipenv shell #fire up virtual environment

(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/django_svelte_jwt_auth> pipenv install django djangorestframework djangorestframework-simplejwt gunicorn whitenoise psycopg2-binary  #install the dependencies

(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/django_svelte_jwt_auth> django-admin startproject backend  #start django project with the name backend

sirneij@pop-os ~/D/P/T/django_svelte_jwt_auth> npm init svelte@next frontend   #start a sveltekit project, I chose skeleton project, activated typescript support, and allowed linters

sirneij@pop-os ~/D/P/T/django_svelte_jwt_auth> cd frontend && npm i  #change directory to frontend and installed dependencies.
Enter fullscreen mode Exit fullscreen mode

If you cloned the project setup on github, ensure you install all the dependencies required.

Section 1: Build the backend and create APIs

Now, let's get to the real deal. We'll be building authentication and authorization API services for the frontend (we'll come back to this later) to consume. To start out, we will create an accounts application in our django project:

(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/d/backend (main)> python manage.py startapp accounts
Enter fullscreen mode Exit fullscreen mode

The add the newly created app to our project's settings.py:

INSTALLED_APPS = [
    ...
    # local apps
    'accounts.apps.AccountsConfig', #add this
]
Enter fullscreen mode Exit fullscreen mode

Proceeding to our application's models.py, we will be subclassing django's AbstractBaseUser to create our custom User model. This is to allow us have full control of the model by overriding the model shipped by django. It is a recommended practice officially. For references, you are persuaded to checkout Customizing authentication in Django, How to Extend Django User Model and Creating a Custom User Model in Django. To achieve this, open up accounts/models.py and populate it with:

# backend -> accounts -> models.py

import uuid
from typing import Any, Optional

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


class UserManager(BaseUserManager):  # type: ignore
    """UserManager class."""

    # type: ignore
    def create_user(self, username: str, email: str, password: Optional[str] = None) -> 'User':
        """Create and return a `User` with an email, username and password."""
        if username is None:
            raise TypeError('Users must have a username.')

        if email is None:
            raise TypeError('Users must have an email address.')

        user = self.model(username=username, email=self.normalize_email(email))
        user.set_password(password)
        user.save()

        return user

    def create_superuser(self, username: str, email: str, password: str) -> 'User':  # type: ignore
        """Create and return a `User` with superuser (admin) permissions."""
        if password is None:
            raise TypeError('Superusers must have a password.')

        user = self.create_user(username, email, password)
        user.is_superuser = True
        user.is_staff = True
        user.is_active = True
        user.save()

        return user


class User(AbstractBaseUser, PermissionsMixin):

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    username = models.CharField(db_index=True, max_length=255, unique=True)
    email = models.EmailField(db_index=True, unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    bio = models.TextField(null=True)
    full_name = models.CharField(max_length=20000, null=True)
    birth_date = models.DateField(null=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    # Tells Django that the UserManager class defined above should manage
    # objects of this type.
    objects = UserManager()

    def __str__(self) -> str:
        """Return a string representation of this `User`."""
        string = self.email if self.email != '' else self.get_full_name()
        return f'{self.id} {string}'

    @property
    def tokens(self) -> dict[str, str]:
        """Allow us to get a user's token by calling `user.token`."""
        refresh = RefreshToken.for_user(self)
        return {'refresh': str(refresh), 'access': str(refresh.access_token)}

    def get_full_name(self) -> Optional[str]:
        """Return the full name of the user."""
        return self.full_name

    def get_short_name(self) -> str:
        """Return user username."""
        return self.username

Enter fullscreen mode Exit fullscreen mode

It's a simple model with all recommended methods properly defined. We just enforce username and email fields. We also ensure that email will be used in place of username for authentication. A prevalent paradigm in recent times. As suggested, you can lookup the details of using this approach in the suggested articles. We also ensure that each user has bio and birthdate. Other fields are basically for legacy purposes. A very important method is the tokens property. It uses RefreshToken from Simple JWT to create a set of tokens to recognize a user. The first being refresh token which tends to "live" relatively longer than its counterpart access. The former will be saved to user's browser's localStorage later on to help recreate access token since the latter is the only token that can authenticate a user but has very short live span. Simple JWT, having been set as our default REST Framework's Default authentication class in our settings.py:

# backend -> backend -> settings.py
...
# REST FRAMEWORK
REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_simplejwt.authentication.JWTAuthentication',)}

Enter fullscreen mode Exit fullscreen mode

knows how to verify and filter the tokens making requests.

It'll be observed that Python's types were heavily used with the help of mypy, a library for static type checking in python. It is not required but I prefer types with python.

Next, we'll make django aware of our custom User model by appending the following to our settings.py file:

# backend -> backend -> settings.py

...
# DEFAULT USER MODEL
AUTH_USER_MODEL = 'accounts.User'
Enter fullscreen mode Exit fullscreen mode

Now, its safe to run migrations:

(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/d/backend (main)> python manage.py makemigrations
Enter fullscreen mode Exit fullscreen mode

If everything goes well, you should see:

Migrations for 'accounts':
  accounts/migrations/0001_initial.py
    - Create model User
Enter fullscreen mode Exit fullscreen mode

Then, migrate to 'really create the database.

(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/d/backend (main)> python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

That's it for this part. Up next is creating the serializers that will be used by our views and endpoints. Stick around please...

Outro

Enjoyed this article, consider contacting me for a job, something worthwhile or buying a coffee ☕. You can also connect with/follow me on LinkedIn.

💖 💪 🙅 🚩
sirneij
John Owolabi Idogun

Posted on February 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related