Protecting views¶
The @multifactor_protected decorator is how you tell django-multifactor
which views need a second factor. It’s a regular function decorator and works
on function-based views, View.as_view() (via method_decorator), and entire
URL trees (via decorator_include).
Signature¶
from multifactor.decorators import multifactor_protected
@multifactor_protected(
factors=0,
user_filter=None,
max_age=0,
advertise=False,
)
def my_view(request): ...
Parameter |
Default |
Effect |
|---|---|---|
|
|
Minimum number of currently-active factors required. May be an |
|
|
A dict passed to |
|
|
Seconds since the most recently verified factor’s |
|
|
When |
The most common shapes¶
Soft advert — encourage uptake¶
@multifactor_protected(factors=0, advertise=True)
def home(request): ...
Users without factors see one info banner inviting them to add a second factor. Users with factors are silently challenged. New deployments often start here to bootstrap adoption.
One factor required — the default for sensitive views¶
@multifactor_protected(factors=1)
def billing(request): ...
Any one active factor counts. The fallback OTP counts. If you need to exclude the fallback, see the snippet in Session model.
Multiple factors — defence in depth for the very sensitive¶
@multifactor_protected(factors=2, max_age=5 * 60)
def export_payroll(request): ...
User must have authenticated two distinct factors within the last 5 minutes. For ops dashboards or financial exports.
Staff-only requirement¶
@multifactor_protected(factors=1, user_filter={"is_staff": True})
def admin_dashboard(request): ...
Non-staff are let straight through; staff are challenged. Useful when MFA adoption is rolling out gradually.
Dynamic factor requirements¶
factors accepts a callable. The callable receives the HttpRequest and
must return an int.
def risk_based_factor_count(request):
# Internal network — no MFA. Off-network — one factor. Suspicious — two.
ip = request.META.get("REMOTE_ADDR", "")
if ip.startswith("10."):
return 0
if request.session.get("recent_failed_logins", 0) > 3:
return 2
return 1
@multifactor_protected(factors=risk_based_factor_count)
def billing(request): ...
Typical inputs to the callable:
Request origin (
request.META["REMOTE_ADDR"], geolocation lookup).Time of day (after-hours = stricter).
User attributes (
request.user.is_staff, group membership).Recent security events read from another model.
Caution
Anything you read inside factors= runs on every request to the
decorated view. Keep it fast — a User.objects.filter(...).count() is
cheap; a third-party HTTP call is not.
Combining with @login_required¶
multifactor_protected lets unauthenticated requests fall through unchanged.
You almost always want @login_required outside it:
@login_required
@multifactor_protected(factors=1)
def billing(request): ...
Decorator order in Python is bottom-up: multifactor_protected runs first,
sees request.user.is_anonymous, returns the wrapped view’s response —
which @login_required then intercepts.
Protecting class-based views¶
Use django.utils.decorators.method_decorator:
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
@method_decorator(multifactor_protected(factors=1), name="dispatch")
class Billing(TemplateView):
template_name = "billing.html"
Or use the mixins, which read more naturally for CBVs — see Mixins.
Protecting an entire URL tree¶
decorator_include from
django-decorator-include
wraps every URL inside an include():
from decorator_include import decorator_include
from multifactor.decorators import multifactor_protected
urlpatterns = [
path("admin/multifactor/", include("multifactor.urls")),
path(
"admin/",
decorator_include(multifactor_protected(factors=1), admin.site.urls),
),
]
This is the cleanest way to MFA-gate Django admin without modifying admin source.
When the decorator doesn’t fire¶
The decorator quietly lets the request through in these cases:
request.user.is_authenticatedisFalse— your auth stack should handle this.is_bypassed(request)returns truthy — see conditional bypass.user_filteris set and the user doesn’t match.factors == 0, the user has noUserKeyrows, andadvertise=False.
All other paths end in either “render the wrapped view” or “redirect to
multifactor:authenticate”.
See also¶
Mixins — the class-based-view equivalent.
Conditional bypass — escape hatches.
multifactor.decoratorsreference — the docstring/API view.