ZevAuth
Sign up

Your backend

Django

Read the signed-in user on every request, protect views with a decorator, serve server-rendered pages and receive webhooks in Django.

pip install "zevauth[django]"

This page covers what Django adds. The client, the backend API and the rules behind every refusal are on Python.

Setup

# settings.py
ZEVAUTH = {
    "ENVIRONMENT_ID": os.environ["ZEVAUTH_ENVIRONMENT_ID"],    # env_live_…
    "SECRET_KEY": os.environ["ZEVAUTH_SECRET_KEY"],            # for the backend API
}

MIDDLEWARE = [
    # …
    "zevauth.django.ZevAuthMiddleware",
]

The middleware sets request.zevauth on every request: the signed-in person, or nobody. It never raises.

Protecting views

from django.http import JsonResponse
from zevauth.django import require_auth

@require_auth
def me(request):
    return JsonResponse({"userId": request.zevauth.user_id})

@require_auth(require_mfa=True)
def payouts(request):
    ...

A refused request answers with the same JSON every ZevAuth library uses: unauthorized (401), session_task_pending (403, with pendingTasks) and mfa_required (403). For a page that should send people to your sign-in screen instead, check request.zevauth.is_signed_in yourself and redirect.

Who is signed in

def home(request):
    auth = request.zevauth
    if auth.is_signed_in and auth.has(role="org:admin"):
        ...

In templates: {% if request.zevauth.is_signed_in %}.

Server-rendered pages

Turn the session bridge on and include its routes:

# settings.py
ZEVAUTH = {
    "ENVIRONMENT_ID": "…",
    "PUBLISHABLE_KEY": os.environ["ZEVAUTH_PUBLISHABLE_KEY"],
    "SESSION_BRIDGE": True,
}

# urls.py
from django.urls import include, path

urlpatterns = [
    # …
    path("zevauth/", include("zevauth.django.urls")),   # /zevauth/session and /zevauth/handshake
]

The middleware then reads the session cookie on every request. When it has expired, a page request is redirected through the handshake, which refreshes it in the browser and returns.

The endpoint is exempt from Django’s CSRF check on purpose: the bridge stops forged posts itself, with its own required header and an Origin check. To use other paths, share cookies across subdomains or allow more origins:

"SESSION_BRIDGE": {
    "BRIDGE_PATH": "/auth/session",
    "HANDSHAKE_PATH": "/auth/handshake",
    "COOKIE_DOMAIN": "example.com",
    "ALLOWED_ORIGINS": ["https://app.example.com", "https://www.example.com"],
},

Mounting the routes elsewhere? Set the two paths to match.

Webhooks

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from zevauth import ZevAuthWebhookError, construct_webhook_event

@csrf_exempt
@require_POST
def zevauth_webhook(request):
    try:
        event = construct_webhook_event(
            request.body,                                # the raw bytes
            request.headers.get("zevauth-signature"),
            os.environ["ZEVAUTH_WEBHOOK_SECRET"],
        )
    except ZevAuthWebhookError:
        return HttpResponse(status=400)
    ...
    return HttpResponse(status=204)

Backend API

from zevauth.django import get_zevauth

user = get_zevauth().api.users.get(request.zevauth.user_id)

get_zevauth() returns the one client built from settings.ZEVAUTH.

Updated at, Sunday, November 1, 2026