djust docs
Browse documentation
intermediate

Accounts

Themed, accessible account pages with a swappable backend: Django's contrib.auth, django-allauth, or your own.

On this page

djust ships account pages that work out of the box: sign in, sign up, email verification, password reset, and social sign-in (GitHub, Google and more). They're themed with your djust theme, work in light and dark mode, and work without JavaScript.

The pages are shared. The backend behind them is a setting:

  • django: plain django.contrib.auth. It covers sign in, sign out, sign up and password reset, with no extra dependency.
  • allauth: django-allauth. It adds email verification by code, social providers, reset by code, rate limits and "remember me", with secure defaults.
  • Your own: subclass djust.auth.accounts.AccountBackend.

Switching the backend never touches a template, and overriding a template never touches the backend. The design is recorded in ADR-039.

This guide covers the account pages. For requiring login on LiveViews and checking permissions, see Authentication & Authorization.

Quick start

With django-allauth (recommended):

pip install "djust[auth-allauth]"
# settings.py
INSTALLED_APPS = [
    # ...
    "djust",
    "djust.theming",
    "djust.auth",               # before allauth, so djust's page skin wins
    "django.contrib.sites",
    "allauth",
    "allauth.account",
    "allauth.socialaccount",    # only if you want social sign-in
]
MIDDLEWARE = [
    # ...
    "allauth.account.middleware.AccountMiddleware",
]
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",  # sign in by email
]
SITE_ID = 1
LOGIN_REDIRECT_URL = "/"        # where people land after signing in (Django's default is /accounts/profile/)

DJUST_CONFIG = {
    "ACCOUNTS": {"BACKEND": "allauth"},
}
# urls.py
from django.urls import include, path

urlpatterns = [
    path("accounts/", include("djust.auth.accounts.urls")),
    # ...
]

Run python manage.py migrate and open /accounts/signup/. New accounts get a verification code by email, and the address is confirmed when they type it in.

To use plain Django instead, set "BACKEND": "django" and leave out the allauth lines. djust.auth and djust.theming are still required: the layout uses {% theme_head %}.

Choosing a backend

djangoallauth
Sign in / sign outyesyes (email or username)
Sign upyesyes
Email verificationnoyes, by code (or link)
Password resetby linkby code
Social sign-innoyes (GitHub, Google, GitLab, Microsoft, …)
Rate limitsnoyes
"Remember me"noyes
Extra dependencynonedjango-allauth

The backend is built once, when the URLconf loads, and reads its OPTIONS then. Changing BACKEND or verification needs a restart (routes and allauth settings are fixed at startup). The hooks it calls, such as signup_validators and is_open_for_signup(), run on every request.

The auth context

Every account page renders from one object, auth, produced by a template tag (no context processor to install):

{% load djust_auth %}
{% auth_context as auth %}
KeyWhat it is
auth.stepThe page: login, signup, verify_email, password_reset, …
auth.formThe page's Django form (the view's form)
auth.providersSocial providers: each has id, label, login_url
auth.nextWhere to go afterwards, already host-checked (off-site values become "")
auth.linksURLs for login, signup, logout, password_reset, present only when the backend supports them
auth.featuresWhat the backend supports, e.g. {"login", "signup", "social"}
auth.errorsThe form's non-field errors
auth.signup_openWhether sign-up is open right now

The URL names are the same for every backend: djust_auth:login, djust_auth:logout, djust_auth:signup, djust_auth:password_reset and, where supported, djust_auth:verify. Link to them without knowing which backend is running:

<a href="{% url 'djust_auth:login' %}?next={{ request.path|urlencode }}">Sign in</a>

Components

Load them with {% load djust_auth %}. Each renders plain, escaped HTML that works without JavaScript. djust_auth/auth.js adds the enhancements.

  • {% auth_providers auth %}: social sign-in buttons with built-in icons for GitHub, Google, GitLab and Microsoft, and a plain button for anything else. The label reads "Continue with GitHub", or "Sign up with GitHub" on the sign-up page. next is carried through.
  • {% auth_divider "or with email" %}: a labelled divider between the provider buttons and the form.
  • {% auth_field form.password %}: a label, the input, help text and the error. It sets autocomplete (username, current-password, new-password, …) and aria-invalid / aria-describedby. Password fields get a Show/Hide button. It's hidden until JavaScript runs, so a page without JavaScript never shows a dead button.
  • {% auth_code_input form.code %}: a one-time-code input with autofill, automatic capitals and paste support. Codes can contain letters (allauth's look like HQPL-VMXW), so it doesn't force a numeric keypad.
  • {% auth_errors form %}: the form-level errors in a role="alert" box. With JavaScript, the first invalid field gets focus.
  • {% auth_links auth %}: "Forgot password?", "New here? Create an account", "Back to sign in". A link only appears when the backend supports it, and "Create an account" disappears when sign-up is closed.

Overriding templates

The kit's templates live in djust.auth:

  • djust_auth/layouts/auth.html: the layout, with blocks title, head, brand, messages, card, aside and footer.
  • djust_auth/pages/*.html: one page per step (login, signup, verify_email, verify_sent, verify_done, password_reset, password_reset_sent, password_reset_confirm, password_reset_done, social_signup, social_error, logout, inactive, signup_closed), used by the django backend and custom backends.
  • allauth/layouts/*.html and allauth/elements/*.html: the allauth skin. allauth keeps its own pages, which carry real logic such as code flows and reauthentication. djust restyles them through allauth's supported override points, so every allauth page, including ones this guide never mentions, gets the kit's look.

Templates are found in this order: your TEMPLATES["DIRS"], then djust.auth, then allauth. To add your logo, override one block:

{# templates/djust_auth/layouts/auth.html #}
{% extends "djust_auth/layouts/auth.html" %}
{% block brand %}<a href="/"><img src="/static/logo.svg" alt="Acme" height="28"></a>{% endblock %}

Override a single page the same way, for example templates/djust_auth/pages/login.html. Classes are prefixed dj-auth-, and colours come from your theme's tokens, so a theme change restyles the pages.

Writing a backend

A backend declares what it supports and returns its views under the stable names. This is a complete backend that signs people in with an emailed link (the email itself is left out):

from django import forms
from django.shortcuts import render
from django.urls import path

from djust.auth.accounts import AccountBackend


class MagicLinkForm(forms.Form):
    email = forms.EmailField(label="Email")


def magic_login(request):
    form = MagicLinkForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        # send a signed, expiring link to form.cleaned_data["email"] here
        return render(request, "djust_auth/pages/verify_sent.html",
                      {"auth_step": "verify_sent", "email": form.cleaned_data["email"]})
    return render(request, "djust_auth/pages/login.html", {"form": form, "auth_step": "login"})


class MagicLinkBackend(AccountBackend):
    name = "magic-link"
    features = frozenset({"login", "logout"})

    def urlpatterns(self):
        from djust.auth.views import logout_view

        return [
            path("login/", magic_login, name="login"),
            path("logout/", logout_view, name="logout"),
        ]
DJUST_CONFIG = {"ACCOUNTS": {"BACKEND": "myproject.auth.MagicLinkBackend"}}

Because signup isn't in features, djust_auth:signup doesn't exist and no page shows "Create an account". Other members you can override:

  • providers(request): return a list of djust.auth.accounts.Provider;
  • is_open_for_signup(request);
  • client_ip(request);
  • extra_urlpatterns(): routes mounted outside the djust_auth namespace.

To change a built-in backend a little, subclass it instead: djust.auth.accounts.backends.django.DjangoBackend or djust.auth.accounts.backends.allauth.AllauthBackend.

Hooks and signals

signup_validators runs before any account is created, with any backend: on the sign-up form, and with allauth also on social sign-up (the "finish signing up" form, and before an automatic social sign-up). Raise ValidationError to refuse; the message appears at the top of the form. data["source"] is "form" or "social", so a check that only makes sense on your own form, such as a captcha, can skip social sign-ups.

from django.core.exceptions import ValidationError


def block_disposable(request, data):
    if data.get("email", "").lower().endswith("@mailinator.com"):
        raise ValidationError("Please use a permanent email address.")


def require_turnstile(request, data):
    if data["source"] == "social":
        return  # the provider already verified a real person
    if not verify_turnstile(request.POST.get("cf-turnstile-response", ""), request):
        raise ValidationError("Please complete the challenge.")


DJUST_CONFIG = {
    "ACCOUNTS": {
        "BACKEND": "allauth",
        "OPTIONS": {"signup_validators": [block_disposable, require_turnstile]},
    },
}

(verify_turnstile stands for your own captcha check.)

is_open_for_signup(request): subclass the backend and return False to close sign-up, for example behind a setting. With the allauth backend this also closes social sign-up.

Signals. These are sent the same way by every backend:

  • djust.auth.signals.user_signed_up, with request and user;
  • djust.auth.signals.email_verified, with request, user and email.
from django.dispatch import receiver

from djust.auth.signals import email_verified


@receiver(email_verified)
def welcome(sender, request, user, email, **kwargs):
    send_welcome_email(user, email)

Security defaults

With "BACKEND": "allauth", djust applies these allauth settings at startup. Any of them you set yourself wins.

Settingdjust defaultWhy
ACCOUNT_EMAIL_VERIFICATION"mandatory"Nobody uses an address they don't own
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLEDTrueNo link for mail scanners to follow; works on another device
ACCOUNT_CONFIRM_EMAIL_ON_GETFalseLink mode: a scanner's GET prefetch can't verify an account
ACCOUNT_PASSWORD_RESET_BY_CODE_ENABLEDTrueSame reasons as verification
ACCOUNT_LOGOUT_ON_GETFalseA crafted link can't sign someone out
ACCOUNT_SESSION_REMEMBERNoneShows "Remember me" and honours it
ACCOUNT_LOGIN_METHODS{"email", "username"}Sign in with either
ACCOUNT_SIGNUP_FIELDSemail, username, passwordOne password field; the Show button replaces "confirm password"
ACCOUNT_ADAPTERdjust's adapterSign-up gate from the backend; strict redirects (below)
ACCOUNT_FORMSdjust's sign-up form (merged into yours)Runs your signup_validators
SOCIALACCOUNT_ADAPTERdjust's social adapterRuns signup_validators before an automatic social sign-up
SOCIALACCOUNT_FORMSdjust's social sign-up form (merged into yours)Runs signup_validators on the "finish signing up" form

ACCOUNT_LOGIN_METHODS and ACCOUNT_SIGNUP_FIELDS are left alone when you still use allauth's older settings (ACCOUNT_AUTHENTICATION_METHOD, ACCOUNT_USERNAME_REQUIRED, ACCOUNT_EMAIL_REQUIRED, …). With a user model that has no username (ACCOUNT_USER_MODEL_USERNAME_FIELD = None), the defaults become email-only. If you bring your own adapter or forms, subclass djust's: check A107 warns otherwise.

Two more behaviours:

  • Redirects stay on your site. allauth normally treats every host your ALLOWED_HOSTS accepts as a safe ?next= target. So ALLOWED_HOSTS = ["*"], or a wildcard such as .example.app whose subdomains users control, turns next into an open redirect. djust's adapter (djust.auth.accounts.backends.allauth_integration.DjustAccountAdapter) allows only the current host, plus any hosts you list in OPTIONS["redirect_hosts"].
  • Rate limits see the real client. allauth's ALLAUTH_TRUSTED_PROXY_COUNT is set from DJUST_TRUSTED_PROXY_COUNT, so allauth and djust agree on the visitor's IP behind a proxy. If your proxy puts the client IP in a header instead (ingress-nginx sets X-Real-IP), you can set allauth's ALLAUTH_TRUSTED_CLIENT_IP_HEADER (allauth 65.14.2 or later) to that header's name. Name only a header your proxy always sets and overwrites. That header only changes allauth's login and sign-up rate limits: djust's own rate limits (WebSocket, SSE, API) and AccountBackend.client_ip() still use DJUST_TRUSTED_PROXY_COUNT, so set both if you rely on those. If a missing header must not block the request (local development, tests, probes), override get_client_ip on your adapter instead, with a fallback; subclass djust's adapter. Check A102 warns if you run behind a proxy with none of these set.

Options (DJUST_CONFIG["ACCOUNTS"]["OPTIONS"]):

OptionEffect
verification"code" (default) or "link"
signup_validatorsList of (request, data) callables
redirect_hostsExtra hosts next may point to

System checks

Account checks are silent unless DJUST_CONFIG["ACCOUNTS"] is set. Details and fixes are in Error codes.

IDLevelMeaning
A100ErrorThe backend can't be imported, or isn't an AccountBackend
A101Errorallauth backend, but allauth isn't installed or its apps or middleware are missing
A102WarningBehind a proxy with no trusted proxy count, client-IP header or adapter override: every visitor shares one rate limit
A103WarningEmail verification is off in production
A104ErrorAccount URLs are included twice
A105InfoA template override extends allauth's base instead of the kit layout
A106Errordjust.auth or djust.theming is missing, or djust.auth comes after allauth
A107WarningYour own allauth adapter or sign-up form doesn't subclass djust's, so its protections are off

Migrating

From djust.auth.urls. include("djust.auth.urls") keeps working as the django backend, and its pages now render instead of raising TemplateDoesNotExist. To get the full kit and the stable names, include djust.auth.accounts.urls instead.

From {% theme_login_page %} and friends. These tags now accept form=. Pass a real form and they render the kit's card with that form's field names:

{% load theme_pages %}
{% theme_login_page form=form action="/accounts/login/" %}

Without form=, they render the old themed mock-up, whose email input doesn't match Django's login form.

From an existing allauth setup. Add "djust.auth" before "allauth", set "BACKEND": "allauth", and replace your include("allauth.urls") with the accounts include. Know two behaviour changes before you deploy:

  • If you didn't set ACCOUNT_EMAIL_VERIFICATION, it becomes "mandatory". Users with unverified addresses are asked for a code at their next sign-in.
  • If you use the new-style settings and didn't set ACCOUNT_SIGNUP_FIELDS, the sign-up form asks for the password once.

Set either setting yourself to keep today's behaviour.

From djust.auth.social.social_auth_providers. This context processor still works but is deprecated. Use {% auth_providers auth %}, whose auth.providers holds the same information.