Integrating Clerk with Django Rest Framework
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 ModelViewSet
that 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.