Django Seedkit

Blog · Auth

How to add magic-link login to Django

The user types an email address. Django sends a one-time link. The user opens the link and the site starts a session. The project needs no reset flow and no social provider.

django-mail-auth is one small dependency with three parts. mailauth holds the backend and the /accounts/login/ views. mailauth.contrib.admin puts the Django admin behind the same flow. mailauth.contrib.user adds an EmailUser model with no password field.

uv add django-mail-auth

Decide the user model first

This decision goes into the first migration, and a later change is expensive. Choose one of three options. Install mailauth.contrib.user and set AUTH_USER_MODEL = "mailauth_user.EmailUser". Write a custom user model with email as the USERNAME_FIELD. Or keep the stock auth.User model.

Two accounts with one email address can log in as each other. The email field on stock auth.User has no unique=True constraint. If two rows hold the same address, MailAuthBackend.authenticate() returns the first match and raises no error. A second signup with another person's address then receives a valid link to that account. To stay on stock auth.User, add a UniqueConstraint in a migration and a validator on the admin form.

Settings

Edit INSTALLED_APPS in place. Do not append. Django resolves admin templates and the admin login view in app order, so the app that overrides must load first. Put mailauth.contrib.admin above django.contrib.admin. In the reverse order the stock password login stays on /admin/, and Django gives no warning.

INSTALLED_APPS = [
    # ... existing entries kept ...
    "mailauth.contrib.admin",   # must precede django.contrib.admin
    "django.contrib.admin",
    "mailauth",
]

AUTHENTICATION_BACKENDS = [
    "mailauth.backends.MailAuthBackend",
    "django.contrib.auth.backends.ModelBackend",
]

LOGIN_URL = "mailauth:login"
LOGIN_REDIRECT_URL = "/"

Then add path("accounts/", include("mailauth.urls")) to the root URL configuration. Give it no namespace argument, because the package declares app_name itself.

Templates and email

The package ships the email body and the subject. Supply three HTML pages: registration/login.html, registration/login_requested.html, and registration/logged_out.html. The package also needs an email backend. Set EMAIL_URL=consolemail:// and the link prints to the runserver output. Without that setting Django uses SMTP on localhost:25, the login form accepts the address, and no mail arrives.

Or give the task to an agent. It reads the same setup from a maintained reference file and applies it to the repository:

/django-seedkit add passwordless magic-link login

The reference holds the full setup, and the default_auto_field pin that keeps makemigrations --check green: auth.md · What Django Seedkit is