FullStack JWT Authentication and Authorization System with Django and SvelteKit
John Owolabi Idogun
Posted on February 7, 2022
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:
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.
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):
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:
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
importuuidfromtypingimportAny,Optionalfromdjango.contrib.auth.modelsimport(AbstractBaseUser,BaseUserManager,PermissionsMixin,)fromdjango.dbimportmodelsfromrest_framework_simplejwt.tokensimportRefreshTokenclassUserManager(BaseUserManager):# type: ignore
"""UserManager class."""# type: ignore
defcreate_user(self,username:str,email:str,password:Optional[str]=None)->'User':"""Create and return a `User` with an email, username and password."""ifusernameisNone:raiseTypeError('Users must have a username.')ifemailisNone:raiseTypeError('Users must have an email address.')user=self.model(username=username,email=self.normalize_email(email))user.set_password(password)user.save()returnuserdefcreate_superuser(self,username:str,email:str,password:str)->'User':# type: ignore
"""Create and return a `User` with superuser (admin) permissions."""ifpasswordisNone:raiseTypeError('Superusers must have a password.')user=self.create_user(username,email,password)user.is_superuser=Trueuser.is_staff=Trueuser.is_active=Trueuser.save()returnuserclassUser(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.emailifself.email!=''elseself.get_full_name()returnf'{self.id}{string}'@propertydeftokens(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)}defget_full_name(self)->Optional[str]:"""Return the full name of the user."""returnself.full_namedefget_short_name(self)->str:"""Return user username."""returnself.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:
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'