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:
- Python(Version==3.9),Django(Version == 4.0.2), Django REST Framework(Version==3.13.1), and REST Framework Simple JWT(Version==5.0.0) at the Backend;
- SvelteKit at the frontend.
Source code
The overall source code for this project can be accessed here:
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
, orvenv
. 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
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.
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
The add the newly created app to our project's settings.py
:
INSTALLED_APPS = [
...
# local apps
'accounts.apps.AccountsConfig', #add this
]
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
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',)}
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'
Now, its safe to run migrations:
(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/d/backend (main)> python manage.py makemigrations
If everything goes well, you should see:
Migrations for 'accounts':
accounts/migrations/0001_initial.py
- Create model User
Then, migrate to 'really create the database.
(django_svelte_jwt_auth) sirneij@pop-os ~/D/P/T/d/backend (main)> python manage.py migrate
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.
Top comments (3)
Nice work. Have you thought about updating the repo to use the latest sveltekit as of August, 27th 2022 with the +page.svelte changes and whatnot? I have been working through your repo updating it to work with the latest and hitting on some snags. Would be interesting to compare your adjustments to mine (completely selfish motivation on my part ;)
I've now updated it.
I am interested. It's a nice initiative.