Integrating Clerk with Django Rest Framework

Lucas Leite
3 min readAug 3, 2023

--

TLDR; Clone the repo.

Our goal

Clerk is a fantastic fully-featured user management platform, and with this tutorial, you will learn how to integrate it with your Django-based application easily.

Setting up a Django-rest-framework app.

To start, let’s install the dependencies:

python --version
# 3.11.4
pip install django djangorestframework django-environ PyJWT requests cryptography

And then start and run a sample project

django-admin startproject clerk_django_integration
cd clerk_django_integration

Add 'rest_framework' to your INSTALLED_APPS setting.

INSTALLED_APPS = [
...
'rest_framework',
]

Run the server

python manage.py migrate
python manage.py runserver

And you should get

Django version 4.2.4, using settings 'clerk_django_integration.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Creating a custom JWTAuthenticationMiddleware

Next, we will create a custom JWTAuthenticationMiddleware to accept requests made using a JWT provided by our Clerk client.

In clerk_django_integration/ create a file called middleware.py

Here’s the code for middleware.py , it features a lightweight ClerkSDK and caching of the certificate. If you are unfamiliar with JWT authentication, I recommend checking out Jwt.io.

# middleware.py
import datetime
from datetime import datetime

import environ
import jwt
import pytz
import requests
from django.contrib.auth.models import User
from django.core.cache import cache
from jwt.algorithms import RSAAlgorithm
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

env = environ.Env()

CLERK_API_URL = "https://api.clerk.com/v1"
CLERK_FRONTEND_API_URL = env("CLERK_FRONTEND_API_URL")
CLERK_SECRET_KEY = env("CLERK_SECRET_KEY")
CACHE_KEY = "jwks_data"


class JWTAuthenticationMiddleware(BaseAuthentication):
def authenticate(self, request):
auth_header = request.headers.get("Authorization")
if not auth_header:
return None
try:
token = auth_header.split(" ")[1]
except IndexError:
raise AuthenticationFailed("Bearer token not provided.")
user = self.decode_jwt(token)
clerk = ClerkSDK()
info, found = clerk.fetch_user_info(user.username)
if not user:
return None
else:
if found:
user.email = info["email_address"]
user.first_name = info["first_name"]
user.last_name = info["last_name"]
user.last_login = info["last_login"]
user.save()

return user, None

def decode_jwt(self, token):
clerk = ClerkSDK()
jwks_data = clerk.get_jwks()
public_key = RSAAlgorithm.from_jwk(jwks_data["keys"][0])
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
options={"verify_signature": True},
)
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("Token has expired.")
except jwt.DecodeError as e:
raise AuthenticationFailed("Token decode error.")
except jwt.InvalidTokenError:
raise AuthenticationFailed("Invalid token.")

user_id = payload.get("sub")
if user_id:
user, created = User.objects.get_or_create(username=user_id)
return user
return None

class ClerkSDK:
def fetch_user_info(self, user_id: str):
response = requests.get(
f"{CLERK_API_URL}/users/{user_id}",
headers={"Authorization": f"Bearer {CLERK_SECRET_KEY}"},
)
if response.status_code == 200:
data = response.json()
return {
"email_address": data["email_addresses"][0]["email_address"],
"first_name": data["first_name"],
"last_name": data["last_name"],
"last_login": datetime.datetime.fromtimestamp(
data["last_sign_in_at"] / 1000, tz=pytz.UTC
),
}, True
else:
return {
"email_address": "",
"first_name": "",
"last_name": "",
"last_login": None,
}, False

def get_jwks(self):
jwks_data = cache.get(CACHE_KEY)
if not jwks_data:
response = requests.get(f"{CLERK_FRONTEND_API_URL}/.well-known/jwks.json")
if response.status_code == 200:
jwks_data = response.json()
cache.set(CACHE_KEY, jwks_data) # cache indefinitely
else:
raise AuthenticationFailed("Failed to fetch JWKS.")
return jwks_data

At the end of setting.py register it like :

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"middlewares.JWTAuthenticationMiddleware",
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
}

Get your secrets from Clerk

If you paid attention to our middleware code, you noticed that there are two variables we have to set:

CLERK_FRONTEND_API_URL
CLERK_SECRET_KEY

To get them, go to Clerk.dev, signup for an account, and go to Dashboard. There, hit the Add Application button, and after completing the wizard go to API KEYS and click Advanced to show your Clerk API URL . Annotate both values and create a file at the root of the project .env as follows:

CLERK_FRONTEND_API_URL=<Your Clerk API Url>
CLERK_SECRET_KEY=<Your secret key>

Also, add to the top of your settings.py file.

import environ, os

env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))

Alternatively, you can export both variables in the terminal. Notice that you have to do this for each new terminal session.

export CLERK_FRONTEND_API_URL=<Your Clerk API Url>
export CLERK_SECRET_KEY=<Your secret key>

Now whenever you start your server, the secrets will be set.

Create a protected view to test our middleware

In clerk_django_integration/ create a file called views.py

Here’s the code for a simple ModelViewSetthat we are going to use to test our authentication:

from django.contrib.auth.models import User
from rest_framework import permissions, serializers, viewsets


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User


class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""

queryset = User.objects.all().order_by("-date_joined")
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]

And register it in urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from clerk_django_integration import views

router = routers.DefaultRouter()
router.register(r"users", views.UserViewSet)

urlpatterns = [
path("api/", include(router.urls)),
path("admin/", admin.site.urls),
]

Run it:

python manage.py runserver

And finally, when you visit http://127.0.0.1:8000/api/users/, you should see the:

Which is expected, as we haven’t provided our credentials yet.

--

--

Lucas Leite

Senior Software Engineer with over a decade of cross-industry experience. Recognized expertise in Python, Typescript, Elixir, React, Node.js, and Django.