Skip to content
Finowl

Django · Email breach checks

Check email breaches in a Django signup form

This example extends UserCreationForm with an email field and runs the Finowl lookup in clean_email, the hook Django calls after the field's own validation. A 400 response becomes a form error, because Finowl only accepts plain addresses with a dotted domain. Any other failure is logged and ignored so signups keep working. Breaches whose DataClasses include Passwords are collected with their Title and BreachDate for later use.

The view does not block registration when a breach is found. Instead it saves the user, logs them in and adds a warning through the messages framework that names the breaches which leaked passwords and suggests a unique password. That keeps the decision with the user while still acting on the result. If you prefer, move the lookup into a Celery task triggered after the user is created.

Steps

  1. 1

    Save the helper module as accounts/finowl.py and set FINOWL_API_KEY in your environment.

  2. 2

    Add SignupForm to accounts/forms.py and route the signup view in urls.py.

  3. 3

    Render messages in your base template so the breach warning appears after the redirect.

  4. 4

    Sign up with an address you control and confirm the warning lists breach titles and dates.

Install
pip install django requests
Check email breaches in a Django signup form
# accounts/forms.py
import logging

from django import forms
from django.contrib.auth.forms import UserCreationForm

from .finowl import FinowlError, finowl_post  # helper from the Django integration page

logger = logging.getLogger(__name__)


class SignupForm(UserCreationForm):
    email = forms.EmailField(max_length=254)

    class Meta(UserCreationForm.Meta):
        fields = ("username", "email")

    def clean_email(self):
        email = self.cleaned_data["email"].strip().lower()
        self.exposed_in = []
        try:
            result = finowl_post("/v1/breaches/email", {"email": email}, timeout=5)
        except FinowlError as exc:
            if exc.status == 400:
                raise forms.ValidationError("Enter a plain address such as name@example.com.")
            if exc.status == 401:
                logger.error("Finowl rejected FINOWL_API_KEY; skipping breach check")
            elif exc.status == 429:
                logger.warning("Finowl %s; retry after %ss", exc.code, exc.retry_after)
            return email  # an unavailable check never blocks a signup

        for breach in result["breaches"]:
            if "Passwords" in breach["DataClasses"]:
                self.exposed_in.append(f"{breach['Title']} ({breach['BreachDate']})")
        return email


# accounts/views.py
from django.contrib import messages
from django.contrib.auth import login
from django.shortcuts import redirect, render


def signup(request):
    form = SignupForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        user = form.save()
        login(request, user)
        if form.exposed_in:
            messages.warning(
                request,
                "Your email appeared in breaches that leaked passwords: "
                + ", ".join(form.exposed_in)
                + ". Use a password you have never used anywhere else.",
            )
        return redirect("home")
    return render(request, "accounts/signup.html", {"form": form})

IN PRODUCTION

Production tips

  • Keep the timeout short in form validation. Five seconds is already a long wait on a signup page, and the helper raises FinowlError on timeouts so clean_email can carry on without the result.

  • Avoid revealing breach details for addresses the visitor has not proven they own. Showing the warning after email verification keeps the form from becoming a way to research other people's addresses.

  • Cache lookups per normalized email with Django's cache framework, so a user who resubmits the form after a password error does not spend a second credit on the same address.

Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Check email breaches in a Django signup form

Grab an API key, paste the example and run it. 100 free credits every month.