Conditional bypass¶
There are legitimate reasons to skip MFA for specific requests — admin
impersonation, automated end-to-end tests, internal network requests.
django-multifactor exposes this via the BYPASS setting. Every bypass is
a hole; use sparingly.
How it works¶
MULTIFACTOR = {
"BYPASS": "myapp.auth.my_bypass_check",
}
BYPASS is a dotted path to a single function taking the current HttpRequest
and returning truthy/falsy. It is consulted by common.is_bypassed(request),
which is called from both the decorator and the mixins. When it returns truthy,
the MFA gate is silently skipped — the request proceeds as if no MFA was
required.
The function runs on every request to every MFA-protected view. Keep it cheap.
Recipe — bypass while impersonating¶
Sites using django-loginas typically want admins to be able to impersonate ordinary users without juggling their MFA factors:
# myapp/auth.py
from loginas.utils import is_impersonated_session
def bypass_when_impersonating(request):
return is_impersonated_session(request)
MULTIFACTOR = {
"BYPASS": "myapp.auth.bypass_when_impersonating",
}
Recipe — bypass during local development¶
def bypass_when_debug(request):
from django.conf import settings
return settings.DEBUG
Caution
This is convenient but dangerous — a single misconfigured production server
with DEBUG=True will silently disable MFA. Prefer the explicit
environment-variable check below for anything that can reach prod.
Recipe — bypass for tests only¶
import os
def bypass_in_tests(request):
return os.environ.get("DJANGO_MULTIFACTOR_DISABLE") == "1"
Then in your CI: DJANGO_MULTIFACTOR_DISABLE=1 pytest. Production never sets
the env var, so it stays on.
Recipe — bypass for internal network¶
import ipaddress
INTERNAL = [ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("192.168.0.0/16")]
def bypass_internal(request):
try:
addr = ipaddress.ip_address(request.META.get("REMOTE_ADDR", ""))
except ValueError:
return False
return any(addr in net for net in INTERNAL)
Warning
REMOTE_ADDR may be your reverse proxy unless you’ve configured
SECURE_PROXY_SSL_HEADER correctly. Validate the chain end-to-end before
trusting it for security decisions.
What bypass does not do¶
It does not create an MFA session entry. Users bypassed via
BYPASShaveactive_factors == []. A view that inspectsactive_factorsdirectly will still see “not authenticated”.It does not prevent the user from voluntarily registering or authenticating factors. They can still use
/admin/multifactor/normally.It does not persist. Each request re-runs the bypass function.
Stacking bypasses¶
BYPASS accepts only a single dotted path. To combine multiple checks,
write a wrapper:
def composite_bypass(request):
from .auth import (
bypass_when_impersonating,
bypass_internal,
)
return bypass_when_impersonating(request) or bypass_internal(request)
Auditing bypasses¶
Every bypass is an MFA hole. At a minimum:
Log when bypass fires, with
user.pk,request.path, and the reason.Add an alert when bypass fires more than expected (e.g. impersonation on a non-admin user).
Make sure your security team knows the bypass conditions exist.
import logging
log = logging.getLogger("multifactor.bypass")
def bypass_when_impersonating(request):
from loginas.utils import is_impersonated_session
if is_impersonated_session(request):
log.info(
"MFA bypassed via impersonation; user=%s path=%s",
request.user.pk,
request.path,
)
return True
return False
See also¶
Threat model — what bypass means for your guarantees.