Skip to content

Customizing

Custom login message (like SMS)

Django Mail Auth can be easily extended. Besides template adaptations it is possible to send different messages like SMS. To make those changes, you will need to write a custom login form.

Custom login form

Custom login forms need to inherit from BaseLoginForm and override the save method.

The following example is for a login SMS. This will require a custom user model with a unique phone_number field:

from django import forms
from django.contrib.auth import get_user_model
from django.template import loader
from mailauth.forms import BaseLoginForm


class SmsLoginForm(BaseLoginForm):
    phone_number = forms.CharField()

    template_name = 'registration/login_sms.txt
    from_number = None

    def __init__(self, *args, **kwargs):
        self.twilio_client = TwilioRestClient(
            settings.TWILIO_SID,
            settings.TWILIO_AUTH_TOKEN
        )
        super().__init__(*args, **kwargs)

    def save(self):
        phone_number = self.cleaned_data['phone_number']
        user = get_user_model().objects.get(
            phone_number=phone_number
        )
        context = self.get_context(self.request, user)

        from_number = self.from_number or getattr(
            settings, 'DEFAULT_FROM_NUMBER'
        )
        sms_content = loader.render_to_string(
            self.template_name, context
        )

        self.twilio_client.messages.create(
            to=user.phone_number,
            from_=from_number,
            body=sms_content
        )

To add the new login form, simply add a new login view to your URL configuration with the custom form:

from django.urls import path
from mailauth.views import LoginView

from .forms import SmsLoginForm

urlpatterns = [
    path("login/sms/", LoginView.as_view(form_class=SmsLoginForm), name="login-sms"),
]

API documentation

Bases: Form

Source code in mailauth/forms.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class BaseLoginForm(forms.Form):
    next = forms.CharField(widget=forms.HiddenInput, required=False)

    def get_login_url(
        self, request: django.http.request.HttpRequest, token: str, next: str = None
    ) -> str:
        """
        Return user login URL including the access token.

        Args:
            request: Current request.
            token: The user specific authentication token.
            next: The path the user should be forwarded to after login.

        Returns:
            User login URL including the access token.

        """
        protocol = "https" if request.is_secure() else "http"
        current_site = get_current_site(request)
        url = "{protocol}://{domain}{path}".format(
            protocol=protocol,
            domain=current_site.domain,
            path=reverse("mailauth:login-token", kwargs={"token": token}),
        )
        if next is not None:
            url += f"?next={urllib.parse.quote(next)}"
        return url

    def get_token(self, user: django.contrib.auth.base_user.AbstractBaseUser) -> str:
        """Return the access token."""
        return MailAuthBackend.get_token(user=user)

    def get_mail_context(
        self,
        request: django.http.request.HttpRequest,
        user: django.contrib.auth.base_user.AbstractBaseUser,
    ) -> dict[str, typing.Any]:
        """
        Return the context for a message template render.

        Args:
            request: Current request.
            user: The user requesting a login message.

        Returns:
            A context dictionary including:
            - ``site``
            - ``site_name``
            - ``token``
            - ``login_url``
            - ``user``

        """
        token = self.get_token(user)
        site = get_current_site(request)
        login_url = self.get_login_url(request, token, self.cleaned_data["next"])
        return {
            "site": site,
            "site_name": site.name,
            "token": token,
            "login_url": login_url,
            "user": user,
        }

    def save(self):
        """
        Send login URL to users.

        Called from the login view, if the form is valid.

        This method must be implemented by subclasses. This method
        should trigger the login URL to be sent to the user.
        """
        raise NotImplementedError

get_login_url(request, token, next=None)

Return user login URL including the access token.

Parameters:

Name Type Description Default
request HttpRequest

Current request.

required
token str

The user specific authentication token.

required
next str

The path the user should be forwarded to after login.

None

Returns:

Type Description
str

User login URL including the access token.

Source code in mailauth/forms.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def get_login_url(
    self, request: django.http.request.HttpRequest, token: str, next: str = None
) -> str:
    """
    Return user login URL including the access token.

    Args:
        request: Current request.
        token: The user specific authentication token.
        next: The path the user should be forwarded to after login.

    Returns:
        User login URL including the access token.

    """
    protocol = "https" if request.is_secure() else "http"
    current_site = get_current_site(request)
    url = "{protocol}://{domain}{path}".format(
        protocol=protocol,
        domain=current_site.domain,
        path=reverse("mailauth:login-token", kwargs={"token": token}),
    )
    if next is not None:
        url += f"?next={urllib.parse.quote(next)}"
    return url

get_mail_context(request, user)

Return the context for a message template render.

Parameters:

Name Type Description Default
request HttpRequest

Current request.

required
user AbstractBaseUser

The user requesting a login message.

required

Returns:

Type Description
dict[str, Any]

A context dictionary including:

dict[str, Any]
  • site
dict[str, Any]
  • site_name
dict[str, Any]
  • token
dict[str, Any]
  • login_url
dict[str, Any]
  • user
Source code in mailauth/forms.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def get_mail_context(
    self,
    request: django.http.request.HttpRequest,
    user: django.contrib.auth.base_user.AbstractBaseUser,
) -> dict[str, typing.Any]:
    """
    Return the context for a message template render.

    Args:
        request: Current request.
        user: The user requesting a login message.

    Returns:
        A context dictionary including:
        - ``site``
        - ``site_name``
        - ``token``
        - ``login_url``
        - ``user``

    """
    token = self.get_token(user)
    site = get_current_site(request)
    login_url = self.get_login_url(request, token, self.cleaned_data["next"])
    return {
        "site": site,
        "site_name": site.name,
        "token": token,
        "login_url": login_url,
        "user": user,
    }

get_token(user)

Return the access token.

Source code in mailauth/forms.py
47
48
49
def get_token(self, user: django.contrib.auth.base_user.AbstractBaseUser) -> str:
    """Return the access token."""
    return MailAuthBackend.get_token(user=user)

save()

Send login URL to users.

Called from the login view, if the form is valid.

This method must be implemented by subclasses. This method should trigger the login URL to be sent to the user.

Source code in mailauth/forms.py
83
84
85
86
87
88
89
90
91
92
def save(self):
    """
    Send login URL to users.

    Called from the login view, if the form is valid.

    This method must be implemented by subclasses. This method
    should trigger the login URL to be sent to the user.
    """
    raise NotImplementedError

Custom User Model

For convenience, Django Mail Auth provides a EmailUser which is almost identical to Django's built-in User but without the password and username field. The email field serves as a username and is -- different to Django's User -- unique and case-insensitive.

Implementing a custom User model

from mailauth.contrib.user.models import AbstractEmailUser
from phonenumber_field.modelfields import PhoneNumberField


class SMSUser(AbstractEmailUser):
    phone_number = phone = PhoneNumberField(
        _("phone number"), unique=True, db_index=True
    )


class Meta(AbstractEmailUser.Meta):
    verbose_name = _("user")
    verbose_name_plural = _("users")
    swappable = "AUTH_USER_MODEL"

Note

Do not forget to adjust your AUTH_USER_MODEL to correct app_label.ModelName.

API documentation

Bases: AbstractUser

Source code in mailauth/contrib/user/models.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class AbstractEmailUser(AbstractUser):
    EMAIL_FIELD = "email"
    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    username = None
    password = None

    email = CIEmailField(
        _("email address"), blank=True, null=True, unique=True, db_index=True
    )
    """Unique and case insensitive to serve as a better username."""

    session_salt = models.CharField(
        max_length=12,
        editable=False,
        default=_get_session_salt,
    )
    """Salt for the session hash replacing the password in this function."""

    def has_usable_password(self):
        return False

    objects = EmailUserManager()

    class Meta(AbstractUser.Meta):
        abstract = True
        permissions = [
            ("anonymize", "Can anonymize user"),
        ]

    def get_session_auth_hash(self):
        """Return an HMAC of the :attr:`.session_salt` field."""
        key_salt = "mailauth.contrib.user.models.EmailUserManager.get_session_auth_hash"
        if not self.session_salt:
            raise ValueError("'session_salt' must be set")
        return salted_hmac(key_salt, self.session_salt, algorithm="sha256").hexdigest()

    def anonymize(self, commit=True):
        """
        Anonymize the user data for privacy purposes.

        This method will erase the email address, first and last name.
        You may overwrite this method to add additional fields to anonymize::

            class MyUser(AbstractEmailUser):
                def anonymize(self, commit=True):
                    super().anonymize(commit=False) # do not commit yet
                    self.phone_number = None
                    if commit:
                        self.save()
        """
        signals.anonymize.send(sender=self.__class__, instance=self)
        self.email = None
        self.first_name = ""
        self.last_name = ""
        update_fields = ["email", "first_name", "last_name"]
        if commit:
            self.save(update_fields=update_fields)
        return update_fields

email = CIEmailField(_('email address'), blank=True, null=True, unique=True, db_index=True) class-attribute instance-attribute

Unique and case insensitive to serve as a better username.

session_salt = models.CharField(max_length=12, editable=False, default=_get_session_salt) class-attribute instance-attribute

Salt for the session hash replacing the password in this function.

anonymize(commit=True)

Anonymize the user data for privacy purposes.

This method will erase the email address, first and last name. You may overwrite this method to add additional fields to anonymize::

class MyUser(AbstractEmailUser):
    def anonymize(self, commit=True):
        super().anonymize(commit=False) # do not commit yet
        self.phone_number = None
        if commit:
            self.save()
Source code in mailauth/contrib/user/models.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def anonymize(self, commit=True):
    """
    Anonymize the user data for privacy purposes.

    This method will erase the email address, first and last name.
    You may overwrite this method to add additional fields to anonymize::

        class MyUser(AbstractEmailUser):
            def anonymize(self, commit=True):
                super().anonymize(commit=False) # do not commit yet
                self.phone_number = None
                if commit:
                    self.save()
    """
    signals.anonymize.send(sender=self.__class__, instance=self)
    self.email = None
    self.first_name = ""
    self.last_name = ""
    update_fields = ["email", "first_name", "last_name"]
    if commit:
        self.save(update_fields=update_fields)
    return update_fields

get_session_auth_hash()

Return an HMAC of the :attr:.session_salt field.

Source code in mailauth/contrib/user/models.py
77
78
79
80
81
82
def get_session_auth_hash(self):
    """Return an HMAC of the :attr:`.session_salt` field."""
    key_salt = "mailauth.contrib.user.models.EmailUserManager.get_session_auth_hash"
    if not self.session_salt:
        raise ValueError("'session_salt' must be set")
    return salted_hmac(key_salt, self.session_salt, algorithm="sha256").hexdigest()

Unique and case insensitive to serve as a better username.

Salt for the session hash replacing the password in this function.

Bases: AbstractEmailUser

Source code in mailauth/contrib/user/models.py
108
109
110
class EmailUser(AbstractEmailUser):
    class Meta(AbstractEmailUser.Meta):
        swappable = "AUTH_USER_MODEL"