Implementing JWT Authentication in Django for a Chrome Extension

Matt Brown
11 min readJul 12, 2024

--

Introduction

In this guide, you’ll learn how to implement JWT (JSON Web Token) authentication for a Chrome Extension using django, dj-rest-auth, and django-allauth.

Not sure you need JWT? Check out this article which explains session-based authentication and is simpler to implement.

While this example focuses on implementation with a Chrome Extension, the concepts would extend to any system with decoupled front and backend modules.

Throughout this tutorial, I have included inline comments in the code and explanations before the code blocks. Reading both should help you understand what’s going on.

I have also included a video walkthrough.

Just want the code? Check out the GitHub Repo.

Video of the guide

Tutorial Overview

In this tutorial, we will walk you through the following steps:

  1. JWT Authentication Best Practices: General discussion of JWT auth techniques and best practices.
  2. Setting Up Django Backend: Development and configuration of a sample Django app to handle JWT authentication.
  3. Configuring Chrome Extension to use JWT: Development of a Chrome Extension that uses the Django backend. Demonstrating how to authenticate, store tokens, and make authenticated API requests.

By the end of this tutorial, you’ll have a working example of a Chrome Extension that uses JWT authentication with a Django backend.

JWT Authentication Best Practices

In this section, we discuss JWT authentication in general. Skip the section, if you want to get straight to the code.

JWT is a stateless authentication strategy in which the server doesn’t store session data. Instead, the token is validated against a secret key stored in the backend application.

JWT uses two types of tokens:

  • Access tokens: Short-lived tokens that are used to authorize the user.
  • Refresh tokens: Longer-lived tokens that are used to obtain a new access token once it expires

Each time a user logs in both tokens are generated.

Token expiration times

The access token is short-lived (e.g. 15 minutes), and the refresh token has a longer expiration time (e.g. 7 days). These values are specified in settings.py. The expiration times would vary based on the application. In this example, we are setting the times to very short for demonstration purposes.

# Example settings.py
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(seconds=10),
"REFRESH_TOKEN_LIFETIME": timedelta(seconds=30),
}

Securely storing tokens

HttpOnly cookies are the most secure way to store and transmit access tokens. They are sent in the header of a request and are not accessible from JavaScript.

If HttpOnly cookies are not used, then the token values are available by running document.cookie from the console. A hacker could gain access to the tokens by injecting malicious code into the website. Always use HttpOnly cookies to mitigate against this risk.

Cross-Origin Resource Sharing (CORS)

When implementing JWT authentication, it’s crucial to handle Cross-Origin Resource Sharing (CORS) appropriately. CORS allows your application to make requests to a different domain than the one that served the web page. To secure your application, ensure that CORS settings only permit trusted domains to make API requests.

In the case of a Chrome extension, you should specify the extension’s unique identifier as a trusted domain in the CORS configuration. For example, if the requests are made from background.js in the extension, this unique identifier ensures that only your extension can communicate with the API. It is generally a good idea to send network requests from background.js in a Chrome Extension.

CSRF_TRUSTED_ORIGINS = [
'<chrome-extension-unique-identifier>'
# e.g. chrome-extension://pnchahhkakickfihmeklhjgeacjhcdii'
]

Setting up Django Backend

For this tutorial, it’s assumed you know the basics of developing a Django application. If you’re new to Django, check out their documentation.

Here are the basic commands for building a Django app.

virtualenv ./env --python=python3.11
source ./env/bin/activate
pip install django
django-admin startproject jwt_chrome_app
cd jwt_chrome_app
python manage.py startapp core
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

You should have a Django app which you can run via python manage.py runserver

Install the necessary libraries

$ pip install dj-rest-auth djangorestframework-simplejwt django-allauth

Update settings

Add the necessary apps toINSTALLED_APPS in settings.py

# settings.py

INSTALLED_APPS = [
...
# The following apps are required:
'django.contrib.auth',
'django.contrib.messages',

"rest_framework_simplejwt",
"rest_framework.authtoken",
"dj_rest_auth",
"dj_rest_auth.registration",
"allauth",
"allauth.account",
]

You can add the code below at the bottom of settings.py.

You will need to update CSRF_TRUSTED_ORIGINS to be the location of your chrome extension. This is automatically assigned when you load the extension. We will come back to this at the end of the tutorial.

# settings.py

from datetime import timedelta
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}


SIMPLE_JWT = {
# NOTE: Here we specify the token expiration times.
# We set them to small values for demonstration purposes
"ACCESS_TOKEN_LIFETIME": timedelta(seconds=10),
"REFRESH_TOKEN_LIFETIME": timedelta(seconds=30),
"TOKEN_BLACKLIST_ENABLED": True,
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"UPDATE_LAST_LOGIN": True,
"SIGNING_KEY": 'aklsdfjlk2k34234lkmlakdfASDFa098442',
"ALGORITHM": "HS256",
}

REST_AUTH = {
"USE_JWT": True,
"JWT_AUTH_COOKIE": "my-access-token",
"JWT_AUTH_REFRESH_COOKIE": "my-refresh-token",
"JWT_AUTH_HTTPONLY": True,
"JWT_AUTH_SECURE": 'lax',
"USER_DETAILS_SERIALIZER": "apps.users.serializers.CustomUserSerializer",
}

AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',

# `allauth` specific authentication methods, such as login by email
'allauth.account.auth_backends.AuthenticationBackend',
]

ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS=False
LOGIN_REDIRECT_URL = '/'

# UPDATE: This is the location of
CSRF_TRUSTED_ORIGINS = [
# NOTE: You will need to update with the unique identifier of your chrome
# extension
'chrome-extension://pnchahhkakickfihmeklhjgeacjhcdii'
]

Configuring login view for django-allauth

Each time the user logs in, the application will generate new access and refresh tokens and save them to the user’s cookies.

In this example, we will override the standard login view from django-allauth to generate and store the JWT tokens each time the user logs in. Alternatively, you can call set_jwt_cookies from your existing login view.

A home view is included so we have a page to send the user to after logging in.

# views.py

from django.shortcuts import render
from django.conf import settings
from django.http import HttpResponse

# Create your views here.
from allauth.account.views import SignupView, LoginView
from rest_framework_simplejwt.tokens import AccessToken, RefreshToken

def set_jwt_cookies(user, response):
refresh = RefreshToken.for_user(user)
cookie_name = settings.REST_AUTH['JWT_AUTH_COOKIE']
response.set_cookie(
cookie_name,
str(refresh.access_token),
httponly=True,
secure=False, # Change to True in production
samesite='lax'
)
refresh_token_name = settings.REST_AUTH['JWT_AUTH_REFRESH_COOKIE']
response.set_cookie(
refresh_token_name,
str(refresh),
httponly=True,
secure=False, # Change to True in production
samesite='lax'
)

class CustomLoginView(LoginView):
def form_valid(self, form):
response = super().form_valid(form)
user = self.request.user
set_jwt_cookies(user, response)
return response

# NOTE: For testing purposes we also include a home view
def home(request):
return HttpResponse("Hello, Django!")

Override allauth urls

We also need to override the existing login view in urls.py. Because accounts/login is listed after accounts/, the standard login view will be overridden by our new view which uses the CustomLoginView.

# jwt_chrome_app/urls.py

from django.urls import path
from django.urls.conf import include
from core.views import CustomLoginView, home

urlpatterns = [
...
path("accounts/", include("allauth.urls")),
path("accounts/login", CustomLoginView.as_view(), name="account_login"),

path("", home, name="home"),
]

Add necessary middleware for allauth

# settings.py

MIDDLEWARE = [
...

"allauth.account.middleware.AccountMiddleware",
]

Run database migrations

Run python manage.py migrate

Verify that JWT tokens are being stored properly

We are at the first checkpoint. Let’s verify that the tokens are stored in cookies.

Each time the user logs in, refresh and access tokens will be stored in cookies on their local machine. The tokens will be sent with each network request. You can view them in the developer tools to verify that they are being properly created and stored.

  1. Start the server python manage.py runserver
  2. Go to localhost:8000/accounts/login and log in with the superuser you created earlier.
  3. Open dev tools -> Network
  4. Reload the page (You should be on the home page at this point)
  5. Click on the request that was just made (should say localhostor 127.0.0.1)

Here you can see the cookies, and the settings we specified. It looks like the cookies are being stored properly!

Setup services for Chrome Extension

The extension will use views from dj-rest-auth to verify and refresh the user’s access token. Add the views shown below. The new ones are the token/verify/ view which validates the access token and the token/refresh/ which gets a new access token using the refresh token. These are the views the Chrome Extension will call for authentication.

Your urls.py should look like this now.

# urls.py

from django.contrib import admin
from django.urls import path
from django.urls.conf import include
from core.views import home, CustomLoginView
from dj_rest_auth.jwt_auth import get_refresh_view
from django.urls import path
from rest_framework_simplejwt.views import TokenVerifyView


urlpatterns = [
path("admin/", admin.site.urls),
path("", home, name="home"),

path("accounts/", include("allauth.urls")),
path("accounts/login", CustomLoginView.as_view(), name="account_login"),

# NEW dj-rest-auth views
path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
path("token/refresh/", get_refresh_view().as_view(), name="token_refresh"),
]

Setting up custom middleware

One tricky piece is that when using HttpOnly cookies, they are strictly stored in headers. However, the dj-rest-auth views require that the access and refresh tokens be included in the request body.

Since HttpOnly cookies are stored in headers, we created custom middleware to move these tokens into the request body for dj-rest-auth views. Add the following middleware to ensure proper handling of the tokens.

This solution was developed by GitHub user Wolf-Byte. Here is a link to the discussion around this issue if you want to learn more.

Create a new file core/middleware.py

# middleware.py

from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
import json

class MoveJWTCookieIntoTheBody(MiddlewareMixin):
"""
for Django Rest Framework JWT's POST "/token-refresh" endpoint --- check for a 'token' in the request.COOKIES
and if, add it to the body payload.
"""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)
return response

def process_view(self, request, view_func, *view_args, **view_kwargs):
# NOTE: If the verify view is called and the access token is in the
# cookies, then move that cookie into the body.
if request.path == '/token/verify/' and settings.REST_AUTH['JWT_AUTH_COOKIE'] in request.COOKIES:

if request.body != b'':
data = json.loads(request.body)
data['token'] = request.COOKIES[settings.REST_AUTH['JWT_AUTH_COOKIE'] ]
request._body = json.dumps(data).encode('utf-8')
else:
# I cannot create a body if it is not passed so the client must send '{}'
pass

return None

class MoveJWTRefreshCookieIntoTheBody(MiddlewareMixin):
"""
for Django Rest Framework JWT's POST "/token-refresh" endpoint --- check for a 'token' in the request.COOKIES
and if, add it to the body payload.
"""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)
return response

def process_view(self, request, view_func, *view_args, **view_kwargs):
# NOTE: If the refresh view is called and the refresh token is in the
# cookies, then move that cookie into the body.
if request.path == '/token/refresh/' and settings.REST_AUTH['JWT_AUTH_REFRESH_COOKIE'] in request.COOKIES:

if request.body != b'':
data = json.loads(request.body)
data['refresh'] = request.COOKIES[settings.REST_AUTH['JWT_AUTH_REFRESH_COOKIE']]

request._body = json.dumps(data).encode('utf-8')
else:
# I cannot create a body if it is not passed so the client must send '{}'
pass

return None

Add new middleware to settings.py.

# settings.py 

MIDDLEWARE = [
...

"allauth.account.middleware.AccountMiddleware",

# Newly created
"core.middleware.MoveJWTCookieIntoTheBody",
"core.middleware.MoveJWTRefreshCookieIntoTheBody",

]

That’s it! Now we just call these views from the Chrome Extension to authenticate users.

Chrome Extension Setup

You can get the Chrome Extension from the GitHub repo. It should work without any updates. Simply load the contents of the chrome_extensionfolder to chrome://extensions. Below is an explanation of how it works.

We are assuming you know the basics of developing a chrome extension, if you need more granular instructions, check out the docs.

Update settings.py to be the unique identifier of the extension

You can get the ID from chrome://extensions

ID: mmjmaoigcacljalllnnhdaecglkcggch is the unique identifier.

Then update the settings in your Django app to the correct identifier.

# settings.py

CSRF_TRUSTED_ORIGINS = [
# NOTE: You will need to update with the unique identifier of your chrome
# extension
'chrome-extension://mmjmaoigcacljalllnnhdaecglkcggch'
]

How to Authorize User

The extension consists of three files manifest.json, background.js, technics.svg.

All Chrome Extensions are required to have a manifest.json file.

// manifest.json
{
"manifest_version": 3,
"name": "JWT authentication example",
"version": "1.0",
"description": "JWT authentication example",
"action": {
"default_icon": "images/technics.svg"
},
"background": {
"service_worker": "scripts/background.js",
"type": "module"
},
"host_permissions": ["http://localhost:8000/*"]
}

All of the logic is contained in background.js. It consists of three functions, activateApp(), verifyToken(), getNewAccessToken().

activateApp() is called when the user clicks the extension icon. If verifyToken() returns valid data, then is calledstartApp(). Otherwise, it opens the login view in a new tab.

startApp() would be the function you call to start your extension

// background.js
let baseUrl = 'http://localhost:8000';
function activateApp(tab) {
verifyToken().then(data => {
if (!data) {
console.log('user not logged in')
// NOTE: Updates the badge on the pinned icon. Here for
// demonstration purposes
chrome.action.setBadgeText({ text: 'Exp' });
chrome.action.setBadgeBackgroundColor({ color: 'red' }); // Optional: Set the badge color

let newUrl = `${baseUrl}/accounts/login/`;
chrome.tabs.create({ url: newUrl });
return null;
}
else {
startApp();
}
}).catch(error => {
console.error('this is the error: ', error);
});
}

chrome.action.onClicked.addListener((tab) => {
console.log('Extension icon clicked');
activateApp(tab);
});

Verify the user’s access token

Now let’s look at verifyToken(). Here we call the verify/token/ view created earlier. We include an empty body because a request body is required for POST requests.

credentials: 'include' automatically adds the cookies to the headers.

If the request returns an invalid response, then we call the getNewAccessToken() which requests a new access token using the refresh token.

// background.js

function verifyToken() {
let url = baseUrl;

return new Promise((resolve, reject) => {
// NOTE: This is the view we specified earlier in the Django app
fetch(`${url}/token/verify/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// NOTE: Include empty body because POST requests require a body.
body: JSON.stringify({}),
// NOTE: Includes the tokens stored in the cookies in the request.
// We don't need to manually add the tokens to the request.
credentials: 'include'
})
.then(response => {
// NOTE: If the response if ok, then we resolve the response
if (response.ok) {
chrome.action.setBadgeText({ text: 'Verif' });
chrome.action.setBadgeBackgroundColor({ color: 'green' }); // Optional: Set the badge color
console.log('Token is valid');
resolve(response.json()); // Token is valid
} else {
// NOTE: Else we try to get a new access token by calling
// the token/refresh view.
console.log('Token is invalid or session expired');
resolve(getNewAccessToken());
}
})

.catch(error => {
console.error('Error:', error);
reject(error);
});
});
}

Refreshing the user’s access token

Next isgetNewAccessToken(), which uses the refresh token to obtain a new access token.

If it is invalid it returns null and the user is redirected to the login screen. The most common reason that a refresh token would be invalid is if it has expired.

// background.js

function getNewAccessToken() {
console.log('getNewAccessToken()');
let url = baseUrl;
// NOTE: We call the token/refresh view specified in the django app
//. Once again, we don't need to explicitly include the access token
//. in the request because it automatically included.
return fetch(`${baseUrl}/token/refresh/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// NOTE: Include empty body because POST requests require a body.
body: JSON.stringify({}),
credentials: 'include' // Necessary to include cookies
})
.then(response => response.json())
.then(data => {
if (data.access) {
chrome.action.setBadgeText({ text: 'Refr' });
chrome.action.setBadgeBackgroundColor({ color: 'blue' }); // Optional: Set the badge color
return data.access;
} else {
console.log('Failed to get a new access token');
return null;
}
})
.catch(error => {
console.error('Error:', error);
return null;
});
}

Congratulations!

You’ve successfully set up JWT authentication for a Chrome Extension. For the full code and more detailed explanations, visit our GitHub Repo. Feel free to share your feedback, ask questions, or suggest improvements in the comments below. Happy coding!

Here’s a quick demo of the app.

--

--

Matt Brown

I'm Matt. A freelance software developer. Contact me if you'd like to work together! matt@gosolucia.com www.MatthewLBrown.com