Adding Clerk Authentication to a Full Stack Application with Django, FastAPI, and Next.js

How to add Clerk Authentication to our full stack app with Django, FastAPI and Next.js.

Engineering

This tutorial builds upon the Full Stack Application with Django, FastAPI, and Next.js by integrating Clerk authentication. We'll extend the existing application to include user authentication and organization management using Clerk.

Steps

1. Set Up the Base Project

1.1 Clone the Original Tutorial Repository

Start by cloning the repository from the original tutorial:

git clone https://github.com/damianhodgkiss/next-django-fastapi-fullstack-tutorial.git

1.2 Confirm Everything Runs

Ensure the existing setup is working correctly:

docker compose up -d

2. Integrate Clerk with the Frontend

2.1 Install Clerk in the Frontend

Add Clerk to the Next.js frontend:

docker compose exec frontend npm install @clerk/nextjs

2.2 Rebuild the Frontend Image

After installation, rebuild the frontend image:

docker compose up --build frontend -d

2.3 Create Clerk Middleware

Create a new file frontend/src/middleware.ts with the following content:

import { clerkMiddleware, createRouteMatcher} from '@clerk/nextjs/server'

export default clerkMiddleware();

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
}

2.4 Add ClerkProvider

Edit frontend/src/app/layout.tsx:

import { ClerkProvider } from "@clerk/nextjs";

// ...

      <body className={inter.className}>
        <ClerkProvider>
          <body className={inter.className}>
            {children}
          </body>
        </ClerkProvider>
      </body>

2.5 Create Sign-In Page

Create a new file frontend/src/app/sign-in/[[...sign-in]]/page.tsx:

import { SignIn } from "@clerk/nextjs";

export default function Page() {
  return (
    <div className="flex justify-center py-24">
      <SignIn />
    </div>
  );
}

2.6 Create Sign-Up Page

Create a new file frontend/src/app/sign-up/[[...sign-up]]/page.tsx:

import { SignUp } from "@clerk/nextjs";

export default function Page() {
  return (
    <div className="flex justify-center py-24">
      <SignUp />
    </div>
  );
}

3. Configure the Backend for Clerk

3.1 Update Backend Dependencies

Edit backend/requirements.txt:

svix==1.24.0
PyJWT[crypto]==2.8.0

Then rebuild the backend services:

docker compose up --build api admin -d

3.2 Create a FastAPI Endpoint for the Clerk Webhook

Edit backend/users/routers.py:

from fastapi import FastAPI, APIRouter, Depends, HTTPException, status, Request
from django.conf import settings
from django.contrib.auth import get_user_model
from svix.webhooks import Webhook, WebhookVerificationError
from .schemas import (
    ClerkWebhook,
    ClerkWebhookEvent,
)
from .models import Organization

router = APIRouter(prefix="/auth", tags=["auth"])


async def verify_clerk_webhook(request: Request):
    headers = request.headers
    payload = await request.body()

    try:
        wh = Webhook(settings.CLERK_WEBHOOK_SIGNING_SECRET)
        msg = wh.verify(payload, headers)
        return ClerkWebhook.model_validate(msg)
    except WebhookVerificationError as e:
        print("Webhook verification failed:", e)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid webhook signature"
        )
    except Exception as e:
        raise e


@router.post("/clerk_webhook/")
def clerk_webhook(event: ClerkWebhook = Depends(verify_clerk_webhook)):
    if event.type in [
        ClerkWebhookEvent.USER_CREATED,
        ClerkWebhookEvent.USER_UPDATED,
        ClerkWebhookEvent.USER_DELETED,
    ]:
        User = get_user_model()
        User.handle_clerk_webhook(event)
    elif event.type in [
        ClerkWebhookEvent.ORGANIZATION_CREATED,
        ClerkWebhookEvent.ORGANIZATION_UPDATED,
        ClerkWebhookEvent.ORGANIZATION_DELETED,
        ClerkWebhookEvent.ORGANIZATION_MEMBERSHIP_CREATED,
        ClerkWebhookEvent.ORGANIZATION_MEMBERSHIP_UPDATED,
        ClerkWebhookEvent.ORGANIZATION_MEMBERSHIP_DELETED,
    ]:
        Organization.handle_clerk_webhook(event)

    return "OK"


def register_routers(app: FastAPI):
    app.include_router(router)

Edit backend/mysaas/asgi.py:

def init(app: FastAPI):
    from users.routers import register_routers as register_user_routers

    register_user_routers(app)

3.3 Create Schemas for Clerk Webhook

Edit backend/users/schemas.py:

from pydantic import BaseModel
from pydantic import ConfigDict
from enum import Enum


class Organization(BaseModel):
    id: int
    name: str

    model_config = ConfigDict(from_attributes=True)


class User(BaseModel):
    id: int
    email: str

    model_config = ConfigDict(from_attributes=True)


class ClerkWebhookEvent(str, Enum):
    USER_CREATED = "user.created"
    USER_DELETED = "user.deleted"
    USER_UPDATED = "user.updated"
    ORGANIZATION_CREATED = "organization.created"
    ORGANIZATION_DELETED = "organization.deleted"
    ORGANIZATION_UPDATED = "organization.updated"
    ORGANIZATION_MEMBERSHIP_CREATED = "organizationMembership.created"
    ORGANIZATION_MEMBERSHIP_DELETED = "organizationMembership.deleted"
    ORGANIZATION_MEMBERSHIP_UPDATED = "organizationMembership.updated"


class ClerkWebhook(BaseModel):
    object: str = "event"
    type: ClerkWebhookEvent
    data: dict

4. Configure Clerk API Keys and Webhook

4.1 Set Up Clerk API Keys

Visit Developers -> API Keys in the Clerk dashboard, ensure the dropdown says Next.js, and click the Copy button.

Paste the keys into frontend/.env.local:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
CLERK_SECRET_KEY=...

4.2 Set Up Webhook Endpoint

Install ngrok if it's not already installed and run it to capture webhook events:

ngrok http 80

Copy the https:// public hostname for the webhook endpoint.

Visit Configure -> Webhooks in the Clerk dashboard and add an endpoint using the ngrok hostname. For example:

https://7bc8-139-213-232-81.ngrok-free.app/api/v1/auth/clerk_webhook/

Subscribe to all organization, user, and organizationMembership events.

Get the signing secret from the [eye] icon and paste it into backend/.env.local:

CLERK_WEBHOOK_SIGNING_SECRET=...

5. Update Backend Models and Admin

5.1 Update User Model

Edit backend/users/models.py:

from django.db import models
from django_use_email_as_username.models import BaseUser, BaseUserManager
from .schemas import ClerkWebhook, ClerkWebhookEvent


class User(BaseUser):
    objects = BaseUserManager()

    clerk_id = models.CharField(max_length=255, blank=True, null=True)

    @classmethod
    def handle_clerk_webhook(cls, event: ClerkWebhook):
        if event.type in [
            ClerkWebhookEvent.USER_CREATED,
            ClerkWebhookEvent.USER_UPDATED,
        ]:
            data = event.data
            clerk_id = data.get("id")
            # lookup user by clerk_id
            try:
                user = cls.objects.get(clerk_id=clerk_id)
            except cls.DoesNotExist:
                primary_email = next(
                    (
                        email.get("email_address")
                        for email in data.get("email_addresses", [])
                        if email.get("id") == data.get("primary_email_address_id")
                    ),
                    None,
                )
                try:
                    # lookup user by email address
                    user = cls.objects.get(email=primary_email)
                    user.clerk_id = clerk_id
                except cls.DoesNotExist:
                    user = cls(clerk_id=clerk_id)
                    user.email = primary_email

            user.first_name = data.get("first_name")
            user.last_name = data.get("last_name")

            user.save()
        elif event.type == ClerkWebhookEvent.USER_DELETED:
            data = event.data
            clerk_id = data.get("id")
            try:
                user = cls.objects.get(clerk_id=clerk_id)
                user.delete()
            except cls.DoesNotExist:
                pass


class OrganizationMembership(models.Model):
    ROLE_CHOICES = [
        ("org:member", "Member"),
        ("org:admin", "Admin"),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    organization = models.ForeignKey("Organization", on_delete=models.CASCADE)
    role = models.CharField(max_length=50, choices=ROLE_CHOICES, default="org:member")

    def __str__(self) -> str:
        return f"{self.user} -  {self.role} in {self.organization}"

    class Meta:
        unique_together = ("user", "organization")


class Organization(models.Model):
    name = models.CharField(max_length=255)
    users = models.ManyToManyField(
        User, through=OrganizationMembership, related_name="organizations"
    )
    clerk_id = models.CharField(max_length=255, blank=True, null=True)

    def __str__(self):
        return self.name

    @classmethod
    def handle_clerk_webhook(cls, event: ClerkWebhook):
        data = event.data
        if event.type in [
            ClerkWebhookEvent.ORGANIZATION_CREATED,
            ClerkWebhookEvent.ORGANIZATION_UPDATED,
        ]:
            clerk_id = data.get("id")
            try:
                organization = cls.objects.get(clerk_id=clerk_id)
            except cls.DoesNotExist:
                organization = cls(clerk_id=clerk_id)

            organization.name = data.get("name")
            organization.save()
        elif event.type == ClerkWebhookEvent.ORGANIZATION_DELETED:
            clerk_id = data.get("id")
            try:
                organization = cls.objects.get(clerk_id=clerk_id)
                organization.delete()
            except cls.DoesNotExist:
                pass
        elif event.type in [
            ClerkWebhookEvent.ORGANIZATION_MEMBERSHIP_CREATED,
            ClerkWebhookEvent.ORGANIZATION_MEMBERSHIP_UPDATED,
        ]:
            org = data.get("organization")
            public_user_data = data.get("public_user_data")

            organization_clerk_id = org.get("id")
            user_clerk_id = public_user_data.get("user_id")
            role = data.get("role", "org:member")

            try:
                organization = cls.objects.get(clerk_id=organization_clerk_id)
                # add user to organization
                user = User.objects.get(clerk_id=user_clerk_id)
                OrganizationMembership.objects.update_or_create(
                    user=user, organization=organization, defaults={"role": role}
                )
            except cls.DoesNotExist:
                pass
            except User.DoesNotExist:
                pass
        elif event.type == ClerkWebhookEvent.ORGANIZATION_MEMBERSHIP_DELETED:
            org = data.get("organization")
            public_user_data = data.get("public_user_data")

            organization_clerk_id = org.get("id")
            user_clerk_id = public_user_data.get("user_id")

            try:
                organization = cls.objects.get(clerk_id=organization_clerk_id)
                user = User.objects.get(clerk_id=user_clerk_id)
                OrganizationMembership.objects.filter(
                    user=user, organization=organization
                ).delete()
            except cls.DoesNotExist:
                pass
            except User.DoesNotExist:
                pass


class OrganizationWithRole(Organization):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        self._role = kwargs.pop("role", None)
        super().__init__(*args, **kwargs)

    @property
    def role(self):
        return self._role

    @classmethod
    def from_org_and_role(cls, organization, role):
        instance = cls(
            **{
                field.name: getattr(organization, field.name)
                for field in organization._meta.fields
            }
        )
        instance._role = role
        return instance

5.2 Update Admin Interface

Edit backend/users/admin.py:

from django.contrib import admin
from django_use_email_as_username.admin import BaseUserAdmin
from .models import User, Organization, OrganizationMembership


class OrganizationMembershipInline(admin.TabularInline):
    model = OrganizationMembership
    extra = 1  # Number of empty forms to display


class OrganizationAdmin(admin.ModelAdmin):
    model = Organization
    inlines = [OrganizationMembershipInline]
    list_display = ("name", "clerk_id")
    search_fields = ("name", "clerk_id")


admin.site.register(User, BaseUserAdmin)
admin.site.register(Organization, OrganizationAdmin)

5.3 Apply Database Migrations

Run the following commands to create and apply migrations:

docker compose exec admin python manage.py makemigrations
docker compose exec admin python manage.py migrate

6. Verify Everything is Running

To ensure everything is set up correctly:

  1. Visit http://localhost/sign-in and you should see a Clerk sign-in page
  2. Visit http://localhost/docs and you should see the /auth/clerk_webhook/ endpoint
  3. Visit http://localhost/admin and you should be able to login and see your User and Organizations

Conclusion

You have successfully integrated Clerk authentication into your full stack application with Django, FastAPI, and Next.js. This setup provides a robust authentication system and allows for user and organization management through Clerk's services.