Back to blog
By Rasul

Django Allauth Tutorial for Production SaaS

Build Django allauth for SaaS with email-first signup, verification codes, social login, passkeys, recovery, rate limits, and lifecycle tests.

A production Django allauth tutorial needs to cover more than installing the package and rendering a login form. Define who owns an identity, when an email becomes trusted, which login methods can reach the same account, how a locked-out user recovers, and what happens when email or an OAuth provider is unavailable. Then encode those decisions in settings, adapters, and lifecycle tests.

This guide builds an email-first account system for a Django SaaS application. Users can sign up with email and password, verify ownership with a code, log in with email or username, connect a trusted social provider, add a passkey, and recover without support editing the database.

What you will build

  1. Install and route django-allauth.
  2. Define one identity contract for signup and login.
  3. Require email verification before granting trusted access.
  4. Customize forms, templates, and adapters without forking allauth.
  5. Add social login without silently merging the wrong accounts.
  6. Add passkeys and recovery codes as an assurance layer.
  7. Keep email, analytics, and other side effects from breaking account creation.
  8. Test the complete account lifecycle, including failures.

The examples use current django-allauth settings verified on July 29, 2026. Djass pins 65.17.0; the latest stable release is 65.18.0. Check the django-allauth release notes before copying the settings into a project on a different version.

Djass's repository history is a useful warning against treating authentication as one-time setup. The project temporarily disabled passkeys while that path was unstable, moved between link and code verification policies, then re-enabled passkeys with recovery codes. Separate fixes stopped a confirmation-email failure from turning signup into a server error and removed unsafe error rendering from overridden templates.

Those changes lead to the framework used here: production allauth has four coordinated layers—policy settings, narrow extension points, branded lifecycle templates, and state-oriented regression tests. Every override adds an upgrade obligation, and every external provider needs a defined failure result.

Step 1: Install and route django-allauth

Install only the capabilities you plan to operate. A password-only project can install the base package. Social login and passkeys need their corresponding extras and provider dependencies.

uv add "django-allauth[socialaccount,mfa]"

Add the account apps, selected social providers, and MFA app. The Django Sites framework is optional for allauth itself, but it remains useful when provider configuration and canonical site identity are managed through Django.

INSTALLED_APPS = [
    # Django apps
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.messages",
    "django.contrib.sessions",
    "django.contrib.sites",

    # django-allauth
    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.github",
    "allauth.mfa",
]

SITE_ID = 1

The official django-allauth quickstart also requires the request context processor, the allauth authentication backend, AccountMiddleware, and the account URLs:

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
]

Add this entry inside the existing TEMPLATES[0]["OPTIONS"]["context_processors"] list:

"django.template.context_processors.request",

Then add this entry to the existing MIDDLEWARE list below AuthenticationMiddleware and MessageMiddleware. Keep Django's session, CSRF, authentication, message, and security middleware in place.

"allauth.account.middleware.AccountMiddleware",
from django.urls import include, path

urlpatterns = [
    path("accounts/", include("allauth.urls")),
]

Run migrations and inspect the URL set before styling anything:

uv run python manage.py migrate
uv run python manage.py check
uv run python manage.py show_urls | grep accounts

If your project does not provide show_urls, open /accounts/login/, /accounts/signup/, and /accounts/password/reset/ manually. Confirm that a POST can create an account in a local test environment and that the expected email appears in the console email backend.

Do not use Django's signed-cookie session backend with allauth. The allauth quickstart warns that verification codes and other secrets may be stored in the session; signed cookies protect integrity but do not encrypt their contents.

Step 2: Define the account identity contract

Choose the identity contract before changing templates. The important question is not “which fields look good on signup?” It is “which identifiers are unique, which can authenticate, and how do social accounts map to local users?”

For an email-first SaaS using Django's default User model, this is a practical baseline:

ACCOUNT_USER_MODEL_USERNAME_FIELD = "username"
ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*"]
ACCOUNT_LOGIN_METHODS = {"email", "username"}
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_SESSION_REMEMBER = True

ACCOUNT_SIGNUP_FIELDS controls what the user supplies. The asterisk marks a required field. The official account configuration reference states that login methods must align with signup fields; allowing email login while collecting no usable email creates a broken contract.

Djass deliberately omits username and password confirmation from its signup form. A repository test proves that password signup still creates a user with a generated username, while the browser only asks for email and one password. That is a product decision, not a universal rule. Keep password confirmation if your risk model values typo prevention more than reducing signup friction. If you permit username login without collecting a username, test the generation and collision behavior explicitly; otherwise the login and signup identifiers do not form a reliable contract.

Write down the contract as a table:

Question Baseline decision Why
What identifies the account? One unique, normalized email plus an internal user ID Email is the user-facing recovery address; the database ID is stable
What can log in? Email or username Existing Django admin and legacy usernames keep working
Who chooses the username? The application Signup stays email-first without removing Django's username field
When is the email trusted? After verification Signup alone does not prove mailbox ownership
Can a provider match a local account by email? Only when that provider's verified-email claim is trusted An untrusted provider must not impersonate a local account

This small matrix prevents later ambiguity around duplicate accounts, support merges, social login, and password recovery.

Keep Django's password validators enabled. Django 6 applies validators in its password forms, including reset and change flows, but does not apply them automatically to direct User.objects.create_user() calls. API or import code that creates passwords must call the validation layer explicitly.

Step 3: Require and operate email verification

Email verification is a state transition, not decoration. A user row can exist while the mailbox remains untrusted. Decide which product actions require a verified address and test that boundary.

For mandatory code-based verification:

ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
ACCOUNT_EMAIL_SUBJECT_PREFIX = ""

Mandatory verification blocks login until the address is verified. The email field must be required in ACCOUNT_SIGNUP_FIELDS. Code verification also supports passkey signup, which requires mandatory verification and code mode in current allauth.

Treat outbound email as production infrastructure:

  • Use a verified sending domain and a monitored provider.
  • Configure a real DEFAULT_FROM_EMAIL.
  • Render both HTML and plain-text messages.
  • Test delivery, bounce, and resend behavior.
  • Give the user a useful screen when delivery is delayed.
  • Keep a support path for correcting a mistyped address.

Do not grant paid access, organization ownership, or another high-impact role only because User was created. Gate that transition on the event your product actually trusts, such as verified email, verified provider identity, or an operator-approved invitation.

The allauth configuration reference distinguishes mandatory, optional, and none. Optional mode still sends mail but permits login before verification. That can work for a low-risk trial, but every permission check must understand the difference between authenticated and verified.

Password reset deserves the same operational treatment. Keep reset responses neutral so strangers cannot use the form to enumerate accounts. Test unknown addresses, expired links, reused links, changed passwords, and sessions created before the reset.

Step 4: Customize at the supported extension points

Start with templates, then forms, then adapters. Override a view only when those extension points cannot express the behavior.

Copy only the templates you intend to change into your template directory:

frontend/templates/
├── account/
│   ├── login.html
│   ├── signup.html
│   ├── password_reset.html
│   └── password_reset_from_key.html
└── mfa/
    ├── index.html
    └── recovery_codes/
        └── index.html

This answers a common People Also Ask question: override an allauth template by placing a file at the same relative template path in a directory Django searches before app templates. Copying every upstream template creates an upgrade burden; copy the smallest set your product owns.

Use ACCOUNT_FORMS when you need field rendering or validation changes:

ACCOUNT_FORMS = {
    "signup": "accounts.forms.CustomSignUpForm",
    "login": "accounts.forms.CustomLoginForm",
}

Use ACCOUNT_ADAPTER for account policy and flow hooks. The official adapter reference documents methods for signup policy, email delivery, user creation, redirects, and validation.

from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings


class AccountAdapter(DefaultAccountAdapter):
    def is_open_for_signup(self, request):
        return settings.ALLOW_SIGNUPS

This is safer than hiding a signup button while leaving the POST endpoint open. Djass tests both the rendered closed state and a direct signup POST to prove that the adapter enforces the policy.

Keep side effects out of save_user() unless they are required for the database transaction. Newsletter subscription, analytics, welcome sequences, and CRM updates should react to a committed account event and tolerate retries. If a side effect is required, define its failure behavior explicitly.

Step 5: Add social login without unsafe account merging

Social login adds a second identity authority. The OAuth provider can assert an email, but your application decides whether that assertion may authenticate an existing local account.

Register each provider explicitly:

SOCIALACCOUNT_AUTO_SIGNUP = True
SOCIALACCOUNT_PROVIDERS = {
    "github": {
        "EMAIL_AUTHENTICATION": True,
        "AUTO_SIGNUP": True,
        "APP": {
            "client_id": env("GITHUB_CLIENT_ID"),
            "secret": env("GITHUB_CLIENT_SECRET"),
        },
    },
}

Never commit provider credentials. Load them from environment or a secret manager and keep local placeholders in .env.example.

EMAIL_AUTHENTICATION is a security decision. The official social-account configuration warns that enabling it lets a provider's verified email authenticate a matching local account. Turn it on only for a provider whose email-verification claim you trust, and prefer provider-specific configuration over a global setting. Do not force every provider email to count as verified when the provider already supplies per-address verification status. Override that signal only after documenting why the provider's normal claim is insufficient.

Decide separately whether to auto-connect the social account. Authenticating a matching email for one session and permanently attaching a provider identity have different recovery consequences. Test:

  • new provider user with a verified email;
  • provider user whose email matches a password account;
  • provider response with no email;
  • provider response with an unverified email;
  • username collision;
  • disconnecting the only remaining login method;
  • provider timeout or denied consent.

Keep SOCIALACCOUNT_LOGIN_ON_GET = False, the current default, so starting an OAuth handshake requires a POST. The official configuration guide strongly recommends this boundary; a plain GET link should not begin a state-changing authentication flow.

Djass uses a social-account adapter to generate a unique username from the verified email when the provider does not supply one. The adapter removes unsupported characters, supplies a random fallback for an empty local part, and checks uniqueness. That is a good example of adapter-owned normalization: it keeps provider quirks out of views and templates.

Step 6: Add passkeys with a recovery path

Passkeys are an authentication factor, not a complete account-lifecycle policy. Before enabling them, decide how users verify the email, name devices, remove a lost authenticator, and recover when every passkey is gone.

Current django-allauth MFA supports TOTP, recovery codes, WebAuthn credentials, and passkey login. WebAuthn is disabled by default. A passkey baseline looks like this:

INSTALLED_APPS += [
    "django.contrib.humanize",
    "allauth.mfa",
]

MFA_SUPPORTED_TYPES = ["webauthn", "recovery_codes"]
MFA_PASSKEY_LOGIN_ENABLED = True
MFA_PASSKEY_SIGNUP_ENABLED = True
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG

The MFA configuration reference requires WebAuthn support, mandatory email verification, and code-based verification for passkey signup. The insecure-origin setting is for local development only. Production WebAuthn needs HTTPS and the correct relying-party origin.

After a user adds a passkey, prompt them to generate and store recovery codes. Do not make support the undocumented recovery factor. If support can remove MFA, define the evidence, authorization, audit log, and notification for that action.

Test passkey enrollment and login in a real browser as well as at the Django view level. WebAuthn binds browser, origin, and device behavior that a unit test cannot fully represent.

Step 7: Make account side effects failure-safe

Authentication must still produce a clear result when analytics, newsletters, or email providers fail. Classify each side effect:

Side effect Blocks account creation? Retry path
Save the user and identity mapping Yes Database transaction
Send mandatory verification Product decision Visible resend and delivery alert
Record an audit event Usually yes for high-risk changes Durable outbox or transactional record
Track signup analytics No Queued, idempotent event
Add to a newsletter No Background retry
Send a welcome sequence No Queue after verified signup

Djass's adapter catches confirmation-email provider failures during initial signup, keeps the user record, suppresses a false “email sent” message, and shows a retry warning. Resend failure is allowed to surface. This is not the only valid policy: a regulated product may choose to roll back account creation instead. The useful part is that initial delivery and explicit resend have different, tested failure contracts.

Queue non-critical work with stable identifiers rather than passing request or user objects. The Django background-task guide explains the worker and retry boundary. If authentication events feed your funnel, use one canonical event name and test its required properties; the PostHog funnel guide shows the Djass event contract. The full Django analytics with PostHog guide explains how to join signup identity, queued server events, and browser intent without making analytics a signup dependency.

Rate limits also need deployment work. Allauth's rate-limit documentation says limits use Django's cache and do not work properly with DummyCache. It also warns that client IP detection depends on your proxy chain. Configure the trusted proxy count or a proxy-set client IP header instead of trusting arbitrary X-Forwarded-For input.

Protect Django admin separately. Installing allauth does not automatically put the default admin login behind allauth MFA, adapters, or account rate limits. Use allauth's secure admin login integration for every AdminSite you expose, and keep admin authorization restricted even after authentication succeeds.

from allauth.account.decorators import secure_admin_login
from django.contrib import admin

admin.site.login = secure_admin_login(admin.site.login)

Step 8: Test the full account lifecycle

A login-page test is not an authentication test suite. Build an acceptance matrix around account states and recovery paths:

Scenario Expected result
Password signup with a new email User created; email unverified; verification challenge shown
Correct verification code Email marked verified; trusted onboarding can continue
Wrong or expired code No verification; retry limit and resend policy enforced
Existing email signup Enumeration policy preserved; no duplicate trusted identity
Unknown password-reset email Neutral response; no account disclosure
Used reset link Rejected; password unchanged
Social login with trusted verified email Existing account authenticated according to policy
Social login with unverified or missing email No unsafe account merge
Passkey enrollment Authenticator stored only after verified flow succeeds
Lost passkey Recovery code or documented support recovery works
Signup disabled GET explains the state; direct POST creates no user
Email provider unavailable The chosen failure policy is visible and recoverable
Analytics worker unavailable Authentication succeeds; event remains retryable

Also test authorization separately. Django's documentation distinguishes authentication—proving who the user is—from authorization—deciding what that user may do. A verified login should not automatically make someone a workspace owner, billing administrator, or staff user.

Run focused tests, Django's system check, and the complete test suite against the same database and cache classes used in production:

uv run pytest apps/pages/tests.py apps/core/tests -q
uv run python manage.py check
uv run pytest -q

Use a real Redis test when relying on cache-backed rate limits. Use a browser test for passkeys and provider redirects. Keep provider calls mocked in the normal suite, then run a small sandbox integration test before launch.

Production checklist

Before opening signup to users, verify:

  • allauth apps, middleware, authentication backends, URLs, and migrations are present;
  • signup fields and login methods describe one coherent identity contract;
  • unique-email and account-enumeration behavior match product policy;
  • email verification mode matches the permissions granted before verification;
  • password validators cover every form and API that sets a password;
  • overridden templates are limited, branded, accessible, and upgrade-reviewed;
  • adapters enforce signup and identity policy server-side;
  • provider credentials come from secret storage;
  • social email authentication is enabled only for trusted providers;
  • social login starts with POST, and Django admin uses a separately secured login;
  • passkey login has recovery codes and an audited support path;
  • insecure WebAuthn origins are disabled outside local development;
  • account and reset email delivery has monitoring and a resend path;
  • rate limits use a real cache and trustworthy client IP configuration;
  • account creation, verification, recovery, provider failure, and duplicate-email cases have automated tests;
  • authenticated and authorized remain separate decisions.

Djass can generate a Django SaaS repository with allauth account templates, GitHub authentication, MFA/passkey foundations, email delivery, analytics, and tests already arranged into known project boundaries. Review the generated repository structure and available generator modules before generation, then configure auth and email environment variables for the deployment. The scaffold does not decide who should receive workspace roles, how strict verification should be, or what support may do during recovery.

If you want that maintained repository shape, see the current Djass lifetime pricing.

Django allauth FAQ

How do you use allauth in Django?

Install django-allauth, add its apps, request context processor, authentication backend, account middleware, URLs, and migrations. Then configure signup fields, login methods, email verification, templates, and provider policy as one account contract. Test signup, verification, reset, social login, and recovery before launch.

Is Django allauth good for a SaaS application?

Yes, when you want maintained account, email, social, MFA, and recovery flows inside Django. It does not define your SaaS authorization model. Workspace roles, subscriptions, entitlements, and support recovery policy remain application responsibilities.

How do you override django-allauth templates?

Create a template at the same relative path, such as templates/account/login.html, in a project template directory that Django searches before app templates. Copy only the templates you own so upstream security and flow changes remain easier to review during upgrades.

Should signup require email verification?

Require it before granting permissions that depend on mailbox ownership. Optional verification can suit a low-risk trial, but the application must keep unverified and verified states distinct. Passkey signup in current allauth requires mandatory, code-based email verification.

Can social login automatically use an existing account?

It can, but only enable email authentication for providers whose verified-email assertion you trust. Otherwise a provider that supplies false email data could authenticate as a local user. Decide separately whether a successful match permanently connects the provider account.

Primary references checked

This tutorial was verified on July 29, 2026 against django-allauth's quickstart, account settings, social-account settings, MFA settings, and rate-limit guidance, plus Django 6's authentication and password-management documentation.