How To Secure HxRequests
HxRequests allow modular, component-level interactions between the frontend and backend. Request signing already stops a client from forging a request for an arbitrary handler, but without per-view access controls a validly-issued request can still be replayed against any view that includes the mixin and doesn’t scope which handlers it accepts.
This guide explains how to secure HxRequests, enforce app boundaries, require authentication when appropriate, and explicitly allow cross-app usage where needed.
For an explanation of why these controls exist and the risks they prevent, see Why HxRequest Security Is Needed.
Ordering Django auth mixins on the view
If you combine HtmxViewMixin with a Django auth mixin
(LoginRequiredMixin, PermissionRequiredMixin,
UserPassesTestMixin), put the auth mixin first:
# Correct: LoginRequiredMixin.dispatch runs before the HTMX handoff, so it
# gates both full page loads AND HTMX requests.
class MyView(LoginRequiredMixin, HtmxViewMixin, ListView):
...
# Trap: the auth mixin is skipped on the HTMX path.
class MyView(HtmxViewMixin, LoginRequiredMixin, ListView):
...
HtmxViewMixin.dispatch hands HTMX requests to the resolved
HxRequest without calling super().dispatch(), so an auth mixin
listed after it never runs for HTMX requests. Full page loads chain through
super().dispatch() normally, so the mixin still gates those — but the
HTMX path would be silently unauthenticated. A Django system check
(hx_requests.W001) warns at startup / manage.py check when the
ordering is wrong.
Global Settings
Global settings define the default security policy for all HxRequests in your project.
Require Authentication
Require users to be authenticated before running any HxRequest. You can also
define exceptions that are allowed without authentication (see Unauthenticated Allowlist).
# settings.py
HX_REQUESTS_REQUIRE_AUTH = True
With HX_REQUESTS_REQUIRE_AUTH = True, unauthenticated users are blocked from
executing HxRequests unless the request is explicitly whitelisted in
HX_REQUESTS_UNAUTHENTICATED_ALLOW (below).
Unauthenticated Allowlist
Define the subset of HxRequests that may be executed without authentication. The structure mirrors the global allowlist shapes:
List/tuple/set of app labels → allow all HxRequests in those apps.
Dict of
{app_label: "__all__"}→ same as above.Dict of
{app_label: ["HxNameA", "HxNameB"]}→ allow only those Hx names.
# settings.py
HX_REQUESTS_UNAUTHENTICATED_ALLOW = {
"app1": "__all__", # all HxRequests in app1
"app2": ["hx_request_1", "hx_request_2"], # specific requests in app2
}
Note
The authentication check runs first. If authentication is required and the user
is not authenticated, only HxRequests listed in HX_REQUESTS_UNAUTHENTICATED_ALLOW
are allowed to proceed to the other access controls.
Enforce Same-App Rule
HX_REQUESTS_ENFORCE_SAME_APP = True
By default, hx_requests can only be triggered from views in the same Django app.
For example, a request defined in app1.hx_requests can only be invoked by
views in the app1 app.
If you disable this rule:
HX_REQUESTS_ENFORCE_SAME_APP = False
HxRequests become callable from any app unless restricted by other rules.
Warning
Disabling HX_REQUESTS_ENFORCE_SAME_APP effectively removes app-level isolation.
Any view can call any registered HxRequest.
Only disable this in controlled environments with strict allowlists in place.
Global Allowlist
The global allowlist defines exceptions to the same-app rule. It lets you mark specific apps or individual HxRequests as trusted and callable across apps.
Two forms are supported:
List form:
Allow all HxRequests from the given apps to run anywhere.
HX_REQUESTS_GLOBAL_ALLOW = ["app1", "app2"]
Dict form:
Map app names to specific HxRequest names (name attribute on HxRequests).
The value "__all__" allows every request in that app.
HX_REQUESTS_GLOBAL_ALLOW = {
"app1": ["safe_hx_request", "other_safe_hx_request"],
"app2": "__all__"
}
Warning
Adding third-party or unreviewed apps here grants them global access. Only include internal apps or trusted apps.
Per-View Controls
Each View class can define its own access rules via an allowed list and an
additive flag.
allowed_hx_requests
The allowed_hx_requests attribute defines which HxRequests
are permitted to be triggered from that view.
class TestView(View):
allowed_hx_requests = ["hx_request_1", "hx_request_2"]
This allows only the specified HxRequests to be called from this view,
regardless of the app they belong to.
use_global_hx_rules
Determines whether the allowed_hx_requests list adds to or replaces
the base same-app/global rules.
Additive (default):
class TestView(View):
allowed_hx_requests = ["hx_request_1", "hx_request_2"]
use_global_hx_rules = True
Allowed if either:
- The HxRequest is in allowed_hx_requests, or
- The request passes the base rules (same-app if enforced, or globally allowed).
Restrictive:
class TestView(View):
allowed_hx_requests = ["hx_request_3", "hx_request_4"]
use_global_hx_rules = False
Only HxRequests in allowed_hx_requests can be called from this view
(regardless of same-app/global rules).
Relaxing Path-Binding
By default, every signed token is bound to the URL path it was rendered on, so it only verifies when replayed back to that same path (see Why HxRequest Security Is Needed). This is automatic — there is nothing to enable.
You only need to relax it when the path Django sees at dispatch can legitimately differ from the path a token was rendered on. That comes up in two shapes.
Disabling Globally
If your whole project sits behind a proxy or middleware that rewrites or strips
part of the path (a locale prefix, a mount sub-path / SCRIPT_NAME)
between the browser and Django, the rendered path and the dispatched path won’t
match. Turn path-binding off project-wide:
# settings.py
HX_REQUESTS_BIND_TOKEN_TO_PATH = False
This disables both minting new bound tokens and enforcing existing ones, so it takes effect immediately — already-rendered pages stop failing without a reload.
Opting Out Per Handler
For a single handler whose token must work from a path other than where it was rendered — e.g. you build the request URL yourself, or reuse one token across several URLs instead of letting the tags emit it — opt that handler out:
class CrossPageHx(BaseHxRequest):
name = "cross_page"
bind_to_path = False
In ordinary usage you never hit either case: the template tags bake the render path into the request URL, so the token is always posted back to the path it was bound to.
Evaluation Order
Authentication gate - If
HX_REQUESTS_REQUIRE_AUTHis True and the user is not authenticated:Allow only if the HxRequest matches
HX_REQUESTS_UNAUTHENTICATED_ALLOW.Otherwise, deny.
Per-view allowlist - If the HxRequest is listed in
allowed_hx_requests, allow. - Ifuse_global_hx_rulesis False and it’s not listed, deny. - Ifuse_global_hx_rulesis True and it’s not listed, proceed to step 3Base rules - Allow if globally allowed (per
HX_REQUESTS_GLOBAL_ALLOW), or - Allow if same-app andHX_REQUESTS_ENFORCE_SAME_APPis True. - Allow ifHX_REQUESTS_ENFORCE_SAME_APPis False.
Summary
Warning
Always follow the principle of least privilege. Require authentication for HxRequests by default, only whitelist unauthenticated requests when they are demonstrably safe, and grant cross-app access only to trusted, internal apps.