Skip to content

HTMX Authentication & Authorization Pattern

The Problem

HTMX follows HTTP 302 redirects transparently. When an HTMX partial request hits an auth decorator (e.g., @login_required) that returns a redirect, HTMX follows it and swaps the entire redirected page (login page, chat page, setup wizard) into the small partial target element. This causes broken UI — for example, the full sidebar rendering inside a dropdown.

This affects any view that can return a redirect when called via HTMX: - Django's @login_required → redirects to LOGIN_URL - Django's @staff_member_required → redirects to admin login - Our @registered_user_required → redirects anonymous users to chat - SetupRequiredMiddleware → redirects to /setup/ when workspace is unconfigured

The Solution: HX-Redirect Header

Instead of returning a bare 302, HTMX-aware decorators return a 200 response with an HX-Redirect header. HTMX handles this by performing a full-page client-side navigation — the redirect works correctly without swapping a full page into a partial target.

All auth decorators live in campus_core/decorators.py.

Available Decorators

@htmx_login_required

Drop-in replacement for Django's @login_required. Use on any HTMX endpoint that requires an authenticated user.

from campus_core.decorators import htmx_login_required

@htmx_login_required
def load_conversations(request):
    ...

@registered_user_required

Requires a real (non-anonymous) authenticated user. Rejects both unauthenticated users and the shared anonymous_user account. Already HTMX-aware.

from campus_core.decorators import registered_user_required

@registered_user_required
def folder_selector(request):
    ...

@htmx_staff_member_required

HTMX-aware wrapper around Django's @staff_member_required. Use on all admin/staff HTMX endpoints.

from campus_core.decorators import htmx_staff_member_required

# Import with alias for minimal diff when replacing Django's decorator:
from campus_core.decorators import htmx_staff_member_required as staff_member_required

@staff_member_required
def settings_general(request):
    ...

_htmx_aware_redirect(request, url)

For redirects inside view logic (not decorators). Checks the HX-Request header and returns the appropriate response type.

from campus_core.decorators import _htmx_aware_redirect

def my_view(request):
    if some_condition:
        return _htmx_aware_redirect(request, "/somewhere/")
    ...

Middleware

SetupRequiredMiddleware (campus_core/middleware.py) is also HTMX-aware. When the workspace is unconfigured and an HTMX request arrives, it returns HX-Redirect instead of a 302 or 503 page.

Rules

  1. Never use Django's @login_required on HTMX endpoints — use @htmx_login_required
  2. Never use Django's @staff_member_required on HTMX endpoints — use @htmx_staff_member_required
  3. Never use bare redirect() in HTMX-serving views — use _htmx_aware_redirect(request, url)
  4. Hide UI elements that require auth for users who can't access them (e.g., the "+" knowledge folder button is hidden for anonymous users in chat_input_inner.html)

How It Works

Normal request:    decorator returns 302 → browser follows redirect → full page loads (correct)
HTMX request:     decorator returns 200 + HX-Redirect header → HTMX navigates full page (correct)
HTMX + bare 302:  HTMX follows redirect → swaps full page into partial target (BROKEN)

Testing

Tests are in apps/main_app/tests/test_htmx_auth.py. Each decorator is tested for: - Normal request + unauthenticated → returns 302 - HTMX request + unauthenticated → returns 200 with HX-Redirect - Authenticated user → passes through to view (200 with content)

The middleware tests verify the same pattern for the setup-required flow.