Skip to main content

Overview

Email verification is a critical security feature that confirms users own the email addresses they register with. django-allauth provides flexible email verification with multiple strategies to balance security and user experience.
Email verification prevents users from registering with email addresses they don’t control, which could be used for spam, impersonation, or account takeover.

Verification Methods

django-allauth offers three main verification approaches:
Most Secure - Users cannot log in until email is verified.
# settings.py
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
Flow:
Best for applications handling sensitive data
Prevents fake accounts effectively
Users may abandon signup if they don’t receive email

Verification Delivery Methods

Users click a link in their email to verify:
# settings.py
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = False  # Default
Email example:
Hello {{ user.username }},

Please confirm your email address by clicking the link below:

{{ verification_url }}

This link will expire in 3 days.
Features:
  • ✅ Works in all email clients
  • ✅ One-click verification
  • ✅ Longer expiration (default: 3 days)
  • ✅ No typing required
Configuration:
# settings.py

# Token expiration
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 3

# Use HMAC tokens (stateless, more secure)
ACCOUNT_EMAIL_CONFIRMATION_HMAC = True  # Default

# Allow verification via GET request
ACCOUNT_CONFIRM_EMAIL_ON_GET = False  # Default (requires POST for safety)
Setting ACCOUNT_CONFIRM_EMAIL_ON_GET = True allows email verification via GET request, which violates HTTP semantics (GET shouldn’t modify state). Only enable if absolutely necessary for compatibility.

Code-Based Verification

Users manually enter a 6-digit code from their email:
# settings.py
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS = 3
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_TIMEOUT = 900  # 15 minutes
Email example:
Hello {{ user.username }},

Your verification code is:

    123456

This code expires in 15 minutes.
Features:
  • ✅ Better for mobile users (no link clicking needed)
  • ✅ More secure (session-based, shorter timeout)
  • ✅ Works even if link clicked on different device
  • ⚠️ Requires manual typing
  • ⚠️ Shorter expiration time
Flow: From source (app_settings.py:102-111):
@property
def EMAIL_VERIFICATION_BY_CODE_ENABLED(self):
    return self._setting("EMAIL_VERIFICATION_BY_CODE_ENABLED", False)

@property
def EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS(self):
    return self._setting("EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS", 3)

@property
def EMAIL_VERIFICATION_BY_CODE_TIMEOUT(self):
    return self._setting("EMAIL_VERIFICATION_BY_CODE_TIMEOUT", 15 * 60)

Advanced Verification Features

Change Email During Verification

Allow users to correct their email if they made a typo:
# settings.py

# Allow changing email during verification
ACCOUNT_EMAIL_VERIFICATION_MAX_CHANGE_COUNT = 2  # Or True/False

# Old boolean setting (deprecated)
# ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True
Values:
  • 0 or False - Cannot change email
  • 2 or True - Can change up to 2 times
  • Any integer - Custom limit
Enumeration risk: With enumeration prevention enabled, changing email during verification has limitations. See source (configuration.rst:243-249):
If enumeration prevention is turned on, no account is created when a user signs up using an already existing email. If the user then were able to change to a new email address that is not taken, we would have to create an account as we did not do so yet. Currently, this is not implemented.

Resend Verification Email

Allow users to request a new verification email/code:
# settings.py

# Allow resending verification
ACCOUNT_EMAIL_VERIFICATION_MAX_RESEND_COUNT = 2  # Or True/False

# Old boolean setting (deprecated)
# ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True
Implementation:
from allauth.account.internal.flows.email_verification import (
    send_verification_email_to_address
)
from allauth.account.models import EmailAddress

def resend_verification(request):
    """Resend verification email to user's primary email."""
    email_address = EmailAddress.objects.get_primary(request.user)
    
    if email_address and not email_address.verified:
        sent = send_verification_email_to_address(
            request, 
            email_address,
            signup=False
        )
        if sent:
            messages.info(request, "Verification email sent!")
        else:
            messages.error(request, "Please wait before requesting another email.")

Verification Rate Limiting

Prevent abuse of verification email sending: From source (app_settings.py:270-276):
if self.EMAIL_VERIFICATION_BY_CODE_ENABLED:
    confirm_email_rl = "1/10s/key"
else:
    cooldown = self._setting("EMAIL_CONFIRMATION_COOLDOWN", 3 * 60)
    confirm_email_rl = None
    if cooldown:
        confirm_email_rl = f"1/{cooldown}s/key"
Configuration:
# settings.py
ACCOUNT_RATE_LIMITS = {
    # For code-based: max 1 per 10 seconds per email
    "confirm_email": "1/10s/key",
    
    # For link-based: max 1 per 3 minutes per email
    # Controlled by ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN
}

# Deprecated setting (still works)
ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN = 180  # 3 minutes
Hard vs. Soft Rate Limiting: From source (flows/email_verification.py:169-187):
def handle_verification_email_rate_limit(
    request, email: str, raise_exception: bool = False
) -> bool:
    """
    For email verification by link, it is not an issue if the user runs into rate
    limits. The reason is that the link is session independent. Therefore, if the
    user hits rate limits, we can just silently skip sending additional
    verification emails, as the previous emails that were already sent still
    contain valid links. This is different from email verification by code. Here,
    the session contains a specific code, meaning, silently skipping new
    verification emails is not an option, and we must hard fail (429) instead.
    """
    rl_ok = consume_email_verification_rate_limit(
        request, email, raise_exception=raise_exception
    )
    if not rl_ok and app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
        raise ImmediateHttpResponse(respond_429(request))
    return rl_ok
Link-based: Rate limit failures are silent (old link still works)
Code-based: Rate limit failures return HTTP 429 (new code needed)

Email Address Model

Model Structure

From source (models.py:20-54):
class EmailAddress(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_("user"),
        on_delete=models.CASCADE,
    )
    email = models.EmailField(
        db_index=True,
        max_length=app_settings.EMAIL_MAX_LENGTH,
        verbose_name=_("email address"),
    )
    verified = models.BooleanField(verbose_name=_("verified"), default=False)
    primary = models.BooleanField(verbose_name=_("primary"), default=False)
    
    class Meta:
        unique_together = [("user", "email")]
        constraints = [
            # Each user can have only one primary email
            UniqueConstraint(
                fields=["user", "primary"],
                name="unique_primary_email",
                condition=Q(primary=True),
            )
        ]
        # If ACCOUNT_UNIQUE_EMAIL = True
        if app_settings.UNIQUE_EMAIL:
            constraints.append(
                # Only one user can have a verified email
                UniqueConstraint(
                    fields=["email"],
                    name="unique_verified_email",
                    condition=Q(verified=True)
                )
            )

Key Methods

Mark an email as verified.From source (models.py:75-82):
def set_verified(self, commit=True):
    """Mark email as verified if no conflicts exist."""
    if self.verified:
        return True
    if self.can_set_verified():
        self.verified = True
        if commit:
            self.save(update_fields=["verified"])
    return self.verified
Checks for conflicts (uniqueness constraint) before verifying.
Set email as primary for the user.From source (models.py:84-99):
def set_as_primary(self, conditional=False):
    """
    Marks the email address as primary. In case of `conditional`, it is
    only marked as primary if there is no other primary email address set.
    """
    old_primary = EmailAddress.objects.get_primary(self.user)
    if old_primary:
        if conditional:
            return False
        old_primary.primary = False
        old_primary.save()
    self.primary = True
    self.save()
    user_email(self.user, self.email, commit=True)
    return True
Also syncs with User model’s email field.
Send verification email for this address.From source (models.py:101-105):
def send_confirmation(self, request=None, signup=False):
    """Send verification email."""
    model = get_emailconfirmation_model()
    confirmation = model.create(self)
    confirmation.send(request, signup=signup)
    return confirmation
Delete email and update user model.From source (models.py:107-121):
def remove(self):
    """Remove email address and sync User model."""
    self.delete()
    if user_email(self.user) == self.email:
        # Find alternative email
        alt = (
            EmailAddress.objects.filter(user=self.user)
            .order_by("-verified")
            .first()
        )
        alt_email = ""
        if alt:
            alt_email = alt.email
        user_email(self.user, alt_email, commit=True)

Manager Methods

from allauth.account.models import EmailAddress

# Get primary email
primary = EmailAddress.objects.get_primary(user)

# Get specific email for user
email = EmailAddress.objects.get_for_user(user, 'test@example.com')

# Check if email exists for user
exists = EmailAddress.objects.filter(
    user=user, 
    email='test@example.com'
).exists()

# Get all verified emails
verified = EmailAddress.objects.filter(user=user, verified=True)

Email Confirmation Tokens

HMAC-Based Tokens (Default)

From source (app_settings.py:468-469):
@property
def EMAIL_CONFIRMATION_HMAC(self):
    return self._setting("EMAIL_CONFIRMATION_HMAC", True)
Advantages:
  • Stateless - No database records needed
  • Scalable - No cleanup jobs required
  • Secure - Uses Django’s signing framework
  • Performant - No database queries to verify
From source (configuration.rst:204-209):
In previous versions, a record was stored in the database for each ongoing email confirmation, keeping track of these keys. Current versions use HMAC based keys that do not require server side state.

Database-Based Tokens (Legacy)

# settings.py
ACCOUNT_EMAIL_CONFIRMATION_HMAC = False
Creates EmailConfirmation records: From source (models.py:136-150):
class EmailConfirmation(EmailConfirmationMixin, models.Model):
    email_address = models.ForeignKey(
        EmailAddress,
        verbose_name=_("email address"),
        on_delete=models.CASCADE,
    )
    created = models.DateTimeField(verbose_name=_("created"), default=timezone.now)
    sent = models.DateTimeField(verbose_name=_("sent"), null=True)
    key = models.CharField(verbose_name=_("key"), max_length=64, unique=True)
Only needed if you require database records for auditing or have specific compliance requirements.

Verification During Login

Mandatory Verification Blocking

When ACCOUNT_EMAIL_VERIFICATION = "mandatory": From source (flows/email_verification.py:281-308):
def send_verification_email_at_login(request: HttpRequest, login: Login) -> bool:
    """
    At this point, it has already been confirmed that email verification
    is needed.
    
    Email verification mails are sent:
    a) Explicitly: when a user signs up
    b) Implicitly: when a user attempts to log in using an unverified
    email while EMAIL_VERIFICATION is mandatory.
    """
    if not login.user:
        sent = send_verification_email_at_fake_login(request, login)
    else:
        sent = send_verification_email_at_real_login(request, login)
    return sent

Auto-Login After Verification

Log users in automatically after they verify their email:
# settings.py
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
This only works when:
  1. User is verifying immediately after signup
  2. Browser session is still active
  3. Using link-based verification (not code-based)
Security considerations from source (flows/email_verification.py:108-147):
def login_on_verification(request, email_address) -> Optional[HttpResponse]:
    """
    Simply logging in the user may become a security issue. If you
    do not take proper care (e.g. don't purge used email
    confirmations), a malicious person that got hold of the link
    will be able to login over and over again and the user is
    unable to do anything about it. Even restoring their own mailbox
    security will not help, as the links will still work.
    
    All in all, we only login on verification when the user that is in the
    process of signing up is present in the session to avoid all of the above.
    This may not 100% work in case the user closes the browser (and the session
    gets lost), but at least we're secure.
    """

Custom Verification URLs

Frontend Verification URLs

Redirect to your frontend application for email verification:
# settings.py
HEADLESS_FRONTEND_URLS = {
    "account_confirm_email": "https://app.example.com/verify-email/{key}",
}
The {key} placeholder is replaced with the actual verification token.

Custom URL Construction

Override URL generation in adapter:
from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.internal.flows.email_verification import (
    get_email_verification_url
)

class MyAccountAdapter(DefaultAccountAdapter):
    
    def get_email_confirmation_url(self, request, emailconfirmation):
        """
        Customize verification URL.
        Called when sending verification emails.
        """
        # Use custom domain for verification
        key = emailconfirmation.key
        return f"https://verify.example.com/confirm/{key}"

Email Templates

Default Templates

django-allauth includes templates for verification emails:
allauth/templates/account/email/
├── email_confirmation_subject.txt
├── email_confirmation_message.txt
└── email_confirmation_message.html

Customizing Email Content

Override templates in your project:
{% load i18n %}
{% autoescape off %}
{% blocktrans %}Verify Your Email Address{% endblocktrans %}
{% endautoescape %}

Available Template Context

Variables available in verification email templates:
VariableDescription
userUser object
activate_urlFull verification URL
keyVerification token
siteCurrent Site object
requestHttpRequest (may be None)
expiration_daysDays until token expires

Programmatic Verification

Verify Email in Code

from allauth.account.models import EmailAddress
from allauth.account.internal.flows.email_verification import verify_email

# Get email address
email_address = EmailAddress.objects.get(
    user=user,
    email='user@example.com'
)

# Verify it
if not email_address.verified:
    success = verify_email(request, email_address)
    if success:
        print("Email verified!")

Indirect Verification

Verify email based on external validation (e.g., OAuth provider):
from allauth.account.internal.flows.email_verification import (
    verify_email_indirectly
)

# User logged in via Google OAuth
# We trust Google's email verification
if provider == 'google':
    verify_email_indirectly(request, user, email='user@gmail.com')

Redirect After Verification

Configure where users go after verifying their email:
# settings.py

# For authenticated users
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = '/dashboard/'

# For anonymous users (e.g., clicked link after session expired)
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/accounts/login/'
Default behavior:
  • Authenticated: Redirects to LOGIN_REDIRECT_URL
  • Anonymous: Redirects to LOGIN_URL

Signals

Hook into verification events:
from allauth.account.signals import email_confirmed, email_confirmation_sent
from django.dispatch import receiver

@receiver(email_confirmed)
def on_email_confirmed(sender, request, email_address, **kwargs):
    """
    Called when email is successfully verified.
    """
    user = email_address.user
    
    # Grant access to features
    user.profile.email_verified = True
    user.profile.save()
    
    # Send welcome email
    send_welcome_email(user)
    
    # Track analytics
    analytics.track(user.id, 'Email Verified')

@receiver(email_confirmation_sent)
def on_verification_sent(sender, request, confirmation, signup, **kwargs):
    """
    Called when verification email is sent.
    """
    # Log for monitoring
    logger.info(
        f"Verification email sent",
        extra={
            'email': confirmation.email_address.email,
            'is_signup': signup,
        }
    )

Testing Email Verification

Console Email Backend

View emails in console during development:
# settings.py (development)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Disable for Tests

# settings.py (test)
ACCOUNT_EMAIL_VERIFICATION = "none"

Mock Verification

# tests.py
from allauth.account.models import EmailAddress

def test_email_verification():
    # Create user
    user = User.objects.create_user('test', 'test@example.com', 'password')
    
    # Manually mark email as verified
    email = EmailAddress.objects.create(
        user=user,
        email='test@example.com',
        verified=True,
        primary=True
    )
    
    # Test verified user behavior
    assert email.verified
    assert user can access protected features

Best Practices

  • Mandatory: For apps handling sensitive data, financial transactions, or PII
  • Optional: For social apps, content platforms, or when UX is critical
  • None: Only for internal tools or development
If your app is mobile-first, code-based verification provides better UX:
  • No link clicking required
  • Works across devices
  • Users stay in your app
Always allow users to request a new verification email:
ACCOUNT_EMAIL_VERIFICATION_MAX_RESEND_COUNT = 3
Balance security with usability:
  • Link-based: 3 days (users might verify later)
  • Code-based: 15 minutes (active verification flow)
Track verification email delivery and completion rates:
@receiver(email_confirmation_sent)
def track_verification_sent(sender, **kwargs):
    metrics.increment('verification_email_sent')

@receiver(email_confirmed)
def track_verification_complete(sender, **kwargs):
    metrics.increment('verification_completed')

Next Steps

Account Management

Learn about email address management and multiple emails per user

Rate Limiting

Configure rate limits for verification email sending