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¶
- Never use Django's
@login_requiredon HTMX endpoints — use@htmx_login_required - Never use Django's
@staff_member_requiredon HTMX endpoints — use@htmx_staff_member_required - Never use bare
redirect()in HTMX-serving views — use_htmx_aware_redirect(request, url) - 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.