Dj-rest-auth + allauth not sending email

Context: I'm setting DRF + dj-rest-auth + allauth + simple-jwt for user authentication.

Desired behaviour: Register with no username, only email. Authorize login only if email is verified with a link sent to email. Social login to be added.

Problem: It seems that confirmation email is not being sent. When I run the following test I see that it wanted to send some email but it's not found anywhere.

Test code:

    client = APIClient()
    url = reverse("rest_register")  # dj-rest-auth register endpoint
    # Register a user
    data = {
        "email": "user1@example.com",
        "password1": "StrongPass123!",
        "password2": "StrongPass123!",
    }
    response = client.post(url, data, format="json")
    assert response.status_code == 201, response.data

    print(response.data)


    # Manually verify the user
    from allauth.account.models import EmailConfirmation

    user = User.objects.get(email="user1@example.com")
    from django.core import mail    
    print(f'Amount of sent emails: {len(mail.outbox)}')
    print(f'Email Confimation exists: {EmailConfirmation.objects.filter(email_address__email=user.email).exists()}')

This prints:

{'detail': 'Verification e-mail sent.'}
Amount of sent emails: 0
Email Confimation exists: False

My code:

core/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('api/auth/', include('authentication.urls')),
    path("admin/", admin.site.urls),

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

authentication/urls.py

from dj_rest_auth.jwt_auth import get_refresh_view
from dj_rest_auth.registration.views import RegisterView, VerifyEmailView
from dj_rest_auth.views import LoginView, LogoutView, UserDetailsView
from django.urls import path
from rest_framework_simplejwt.views import TokenVerifyView

urlpatterns = [
    path("register/", RegisterView.as_view(), name="rest_register"),
    path("register/verify-email/", VerifyEmailView.as_view(), name="account_email_verification_sent"), # Docs says I must add if email confirmation is mandatory

    path("login/", LoginView.as_view(), name="rest_login"),
    path("logout/", LogoutView.as_view(), name="rest_logout"),

    path("user/", UserDetailsView.as_view(), name="rest_user_details"),

    path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
    path("token/refresh/", get_refresh_view().as_view(), name="token_refresh"),
]

Custom User model:

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("The Email field must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        return self.create_user(email, password, **extra_fields)

class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True) # Some SO question fixed this setting is_active default value to True
    is_staff = models.BooleanField(default=False)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = UserManager()

Relevant settings:

SITE_ID = 1
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    "django.contrib.sites",
    'django.contrib.staticfiles',    

    # Django REST Framework
    "rest_framework",
    "rest_framework.authtoken",
    # Simple JWT
    "rest_framework_simplejwt",

    # Allauth
    "allauth",
    "allauth.account",
    "allauth.socialaccount",

    # dj-rest-auth
    "dj_rest_auth",
    "dj_rest_auth.registration",

    # CORS
    "corsheaders",

    # Local apps
    "authentication.apps.AuthenticationConfig",
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',

    # Allauth
    'allauth.account.middleware.AccountMiddleware',

    # CORS
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
]

ROOT_URLCONF = 'core.urls'

EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # for testing

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL  = "authentication.User"

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,
    "UPDATE_LAST_LOGIN": True,
    "SIGNING_KEY": "complexsigningkey",  # generate a key and replace me
    "ALGORITHM": "HS512",
}

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ]
}

# Dj-rest-auth settings
REST_AUTH = {
    "USE_JWT": True,
    "JWT_AUTH_HTTPONLY": False,
    "REGISTER_SERIALIZER": "authentication.serializers.RegisterSerializer"
}

# Allauth settings
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
ACCOUNT_LOGIN_METHODS = {'email'}
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_CONFIRMATION_HMAC = True # This is the default. But just to be explicit.

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

Problem was that the test shown was the second test that sends a register POST request with the same email address. That is a problem because even though django db is flushed between tests when using something like @pytest.mark.django_db decorator, it does not flush django cache. Allauth, before sending the verification email, checks if another email has been recently sent to that address and that info is stored in a django.core.cache ConnectionProxy object.

Solution is using cache.clear() where cache is obtained with from django.core.cache import cache . You can write that line at the start of each test, or better yet, add a fixture at the start of your testing file:

@pytest.fixture(autouse=True)
def clear_cache_before_each_test():
    """Fixture to clear the Django cache before each test."""
    cache.clear()
Back to Top