Django Fraud Detection: Middleware That Fails Open

A production-shaped Django integration for VPN, proxy and antidetect detection: per-path strictness, DRF wiring, and the failure handling most examples omit.

Close-up of a gypsy jazz guitar with a D-shaped soundhole, lit in warm light on a dark background
Photo: Old Gypsy Acoustic Guitar by Dejan Krsmanovic, CC BY 2.0, cropped. Via Wikimedia Commons.
On this page
  1. Step 1: Add the script to the page with your form
  2. Step 2: Send the check from your server
  3. The middleware
  4. The view decides what review means
  5. Django REST Framework
  6. Step 3: Submit your form once
  7. Switching to enforce
  8. An IP address without a visit

Most integration examples show the happy path and stop. This one is written the way you would actually deploy it in Django: a small client module, middleware guarded by path with different strictness for different actions, a DRF permission, and a check that never turns a Maskbreak problem into a failed request of your own. It starts in watch mode, where every check is logged and nothing is refused.

Three steps, the same three the Maskbreak console lists: add the script to the page with your form, send the check from your server, and submit your form once.

Step 1: Add the script to the page with your form

Load the script on the page that holds the form and put class="monocle-enriched" on the <form> element itself, never on an input inside it.

html
<!-- templates/base.html -->
<script async src="https://maskbreak.com/assets/sentinel.js"></script>

<!-- templates/registration/signup.html -->
<form method="post" action="{% url 'signup' %}" class="monocle-enriched">
  {% csrf_token %}
  <input type="email" name="email" required>
  <input type="password" name="password" required>
  <button type="submit">Create account</button>
</form>

The script adds three hidden fields to the form: monocle (the network token), sentinel_fp (the browser check id) and sentinel_tz (the time zone). They arrive in request.POST like any other field. If a JavaScript frontend posts JSON instead, call await window.Sentinel.collect() (after checking that window.Sentinel exists) and put its token, fingerprintEventId and tz in the body; the code below reads both spellings.

If the site sends a Content-Security-Policy (django-csp, or a header from your proxy), add Maskbreak's hosts: https://maskbreak.com, https://mcl.maskbreak.com, https://*.mcl.maskbreak.com and https://mesu.maskbreak.com plus 'wasm-unsafe-eval' in script-src; the mcl hosts (https and wss) and mesu in connect-src; https://mesu.maskbreak.com blob: in worker-src.

Step 2: Send the check from your server

One function makes the call, with requests (pip install requests if the project does not have it yet). It maps the form's field names to the API's and calls even when a field is missing: a check that arrives without a token or a browser check id is how the console tells you which half is missing. It never raises.

python
# yourapp/maskbreak.py: server-side only
import logging
import os

import requests

log = logging.getLogger("maskbreak")
MODE = os.environ.get("MASKBREAK_MODE", "watch")  # "watch" or "enforce"
_session = requests.Session()


def check_visitor(data, email=None, account_id=None):
    """Send the browser evidence to Maskbreak. Returns the verdict (a dict),
    or None when there is none. Never raises."""
    payload = {
        "token": data.get("monocle") or data.get("token") or "",
        "fingerprintEventId": data.get("sentinel_fp") or data.get("fingerprintEventId"),
        "tz": data.get("sentinel_tz") or data.get("tz"),
        "email": email,
        "accountId": account_id,
    }
    result = None
    try:
        resp = _session.post(
            "https://maskbreak.com/v1/evaluate",
            json={k: v for k, v in payload.items() if v is not None},
            headers={
                "Authorization": "Bearer " + os.environ.get("MASKBREAK_API_KEY", ""),
                "Content-Type": "application/json",
            },
            timeout=3,
        )
        if resp.ok:
            result = resp.json()
        else:
            log.warning("maskbreak HTTP %s", resp.status_code)  # 401: the key
    except (requests.RequestException, ValueError) as exc:
        log.warning("maskbreak unavailable: %s", type(exc).__name__)
    # One line per check. Never log the key, the token or form data.
    log.info("maskbreak mode=%s decision=%s reasons=%s device_evidence=%s degraded=%s",
             MODE,
             result.get("decision") if result else None,
             result.get("reasons") if result else None,
             result.get("device_evidence") if result else None,
             bool(result.get("degraded")) if result else None)
    return result


def clean_allow(result):
    """A complete, production allow: what a strict action needs in enforce mode."""
    return bool(result) and result.get("decision") == "allow" \
        and result.get("device_evidence") == "ok" \
        and not result.get("degraded") and not result.get("test")

Django's default logging drops INFO from your own loggers, so turn this one on in settings.py, and set MASKBREAK_API_KEY to your live key in the environment, never in settings.py or the repository:

python
# settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {"maskbreak": {"handlers": ["console"], "level": "INFO"}},
}
Try it

Wire it into your own app: a free key returns decision, risk_score and reasons for every visit, 10,000 checks a month on the Free plan, no card.

Get an API key

The middleware

One class, guarded by path prefix so product pages, static files and health checks never call out, and parameterised by how strict each group of routes should be. It reads the evidence from the posted form or a JSON body, so the submitted email travels with it. In watch mode it records the verdict on the request and always continues.

python
# yourapp/middleware.py
import json

from django.http import JsonResponse

from .maskbreak import MODE, check_visitor, clean_allow

# Enforce mode: a strict path continues only on a clean, complete allow;
# a guarded path refuses only a block. A single global threshold is
# always wrong somewhere.
STRICT = ("/withdraw", "/transfer", "/promo/redeem")
GUARDED = ("/signup", "/checkout")


def _evidence(request):
    if request.content_type == "application/json":
        try:
            data = json.loads(request.body or b"{}")
        except ValueError:
            return {}
        return data if isinstance(data, dict) else {}
    return request.POST


class MaskbreakMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        path = request.path
        strict = path.startswith(STRICT)
        # Cheap checks first: reads and unguarded paths never call out.
        if request.method != "POST" or not (strict or path.startswith(GUARDED)):
            return self.get_response(request)

        data = _evidence(request)
        email = data.get("email")
        result = check_visitor(
            data,
            email=email if isinstance(email, str) else None,
            account_id=str(request.user.pk) if request.user.is_authenticated else None,
        )
        request.maskbreak = result

        if MODE == "enforce":
            if result and result.get("decision") == "block":
                return JsonResponse({"error": "Request declined"}, status=403)
            if strict and not clean_allow(result):
                return JsonResponse({"error": "Additional verification required"}, status=409)
        return self.get_response(request)   # watch mode: always

Register it after authentication, so request.user is populated and device-to-account linking works:

python
# settings.py
MIDDLEWARE = [
    # ...
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "yourapp.middleware.MaskbreakMiddleware",
]

The view decides what review means

The middleware refuses only what it was told to; what a review costs belongs in the view, where you can see what is at stake:

python
# yourapp/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST

from .maskbreak import MODE


@require_POST
def signup(request):
    form = SignupForm(request.POST)
    if not form.is_valid():
        return JsonResponse({"errors": form.errors}, status=400)
    user = form.save()
    verdict = getattr(request, "maskbreak", None)
    # 'review' is not 'block': step up instead of refusing.
    if MODE == "enforce" and verdict and verdict.get("decision") == "review":
        require_email_verification(user)
    return JsonResponse({"ok": True})

Passing email adds email.disposable to the result and escalates a clean verdict to review on a burner domain. It never blocks on its own, and it should not; the reasoning is in flag it, don't block on it. The reason list goes to your logs, not to the response: telling a refused client which signal fired is free tuning feedback for whoever is probing you.

Django REST Framework

For DRF views, a permission class keeps the check declarative and per-view; use it instead of listing those paths in the middleware, or each request is checked twice. request.data is already parsed, JSON or form:

python
# yourapp/permissions.py
from rest_framework.permissions import SAFE_METHODS, BasePermission

from .maskbreak import MODE, check_visitor


class VisitorCheck(BasePermission):
    message = "Request refused by risk checks."

    def has_permission(self, request, view):
        if request.method in SAFE_METHODS:
            return True
        data = request.data if hasattr(request.data, "get") else {}
        result = check_visitor(
            data,
            account_id=str(request.user.pk) if request.user.is_authenticated else None,
        )
        request.maskbreak = result
        if MODE != "enforce":
            return True          # watch: log only
        return not (result and result.get("decision") == "block")
python
class CheckoutView(APIView):
    permission_classes = [IsAuthenticated, VisitorCheck]

A JSON client sends token, fingerprintEventId and tz from Sentinel.collect() in the body; check_visitor reads them under either name.

Step 3: Submit your form once

Run the app on your computer or deploy it, open the page in an ordinary browser, wait a moment, and submit the form. The first check then appears in the Maskbreak console (if half of it did not arrive, the console says which half), and your log shows one maskbreak line. degraded=True means no network token arrived (the script was blocked, or the class is missing or on an input); device_evidence=not_sent means no sentinel_fp arrived (submitted before the browser check finished, or blocked); device_evidence=ok with degraded=False is a complete check.

Switching to enforce

Do not enforce everywhere at once. A week of watch mode gives you your own baseline: what share of genuine traffic lands in review, which reasons dominate, and whether your own QA, staff tooling or office VPN trips it. Then set MASKBREAK_MODE=enforce and redeploy; the same code refuses a block, and the strict paths hold anything that is not a clean, complete, production allow (a fixed test token such as test_clean comes back with test: true and never passes them).

A mis-set threshold discovered in production on every route at once is a revenue incident; discovered on one route in watch mode it is a config change.

An IP address without a visit

For log enrichment or batch review of yesterday's signups there is GET /v1/lookup/<ip>. It is an address lookup, not a visitor check: it covers Tor exits and cloud servers only, never a VPN, a proxy or the browser, and it is no substitute for the three steps above.

Questions people ask

Should the middleware fail open or closed?
Open, for almost every action. If the detection API is unreachable and you fail closed, every signup and checkout during the outage fails; if you fail open, a window of traffic goes unscreened while your rate limits and post-hoc review still apply. The second is nearly always the cheaper incident. The exception is actions that are both irreversible and high-value, where the better answer is to queue for manual review rather than auto-approve or auto-refuse.
Where should the check live — middleware or the view?
Both, for different jobs. The middleware sends the check for the paths you list, reading the posted form or JSON body so the email travels with it, and in enforce mode refuses by path: a block everywhere, anything short of a clean allow on strict paths. The view decides what a review means for its action, such as a step-up at signup. DRF views can use a permission class instead, so a request is not checked twice.
Do I need the Python SDK?
No. This guide calls the HTTP API with requests, so the check goes out on every request, including one that arrived without a token, which is what lets the console show which half is missing. The SDK helpers need a token and do not forward the time zone. The SDK is on PyPI as sentinelsup, imported as sentinel, if you prefer it elsewhere.
How do I keep this off the fast path for anonymous browsing?
Guard by method and path, and return early. The middleware does a cheap check on request.method and request.path and hands straight back to get_response for anything that is not a POST to a listed path. Product pages, static assets and health checks should never reach an outbound call: a fraud check on a page view is latency you are paying for nothing.
How should I roll this out to an existing app?
In watch mode, the default: the middleware sends every check, logs the verdict and refuses nothing. After a few days you have your own baseline: what proportion of genuine traffic lands in review, which reason codes dominate, and whether internal QA or staff tooling trips it. Then set MASKBREAK_MODE=enforce, starting with the paths that are actually costing money, and expand.

Paste it in, then watch the verdicts

The public sk_test_sandbox key returns the documented allow, review and block shapes with no account, so the failure path is testable before you go live. SDKs for Node, Python and PHP, or plain HTTP. The API reference and the Free plan cover the rest.

Get started freeRead the API docs