Code


Version 7 (modified by gavinwahl@…, 7 months ago) (diff)

--

Adding an Email-based authentication model

ContribAuthImprovements described and implemented a set of changes to allow for pluggable User models in contrib.auth. One of the primary motivators behind this change was to make it easy to implement email-based authentication. To make this even easier, #20824 describes an enhancement for Django to ship with a ready-to-use Email-based login model.

Some django-developer discussions that cover this topic:

A number of solutions have been proposed. Here is a summary of the most viable candidates:

Option 1: Simple auth.User analog

Feature branch: https://github.com/tanderegg/django/tree/ticket_20824_master

This option is a simple analog to the auth.User model. In auth_email.models, there is an AbstractUser class that inherits from AbstractBaseUser and PermissionsMixin, and contains the fields email, first_name, last_name, is_active, and is_staff, basically identical to auth.User with the exception of having no username. the auth_email.models.User class then extends the AbstractUser class to make it instantiable and swappable. A custom UserManager class adds in methods for creating users and superusers.

The auth_email.admin.UserAdmin class inherits from auth.admin.UserAdmin and overides the appropriate members and methods, including pointing the class to the custom UserCreationForm and UserChangeForm in auth_email.forms. These forms are again nearly identical to the ones in auth.forms.

The approach I took is very similar to the one used in https://github.com/Liberationtech/django-libtech-emailuser. The primary difference is that django-libtech-emailuser does not contain a new abstract class, and instead contains a single concrete class EmailUser.

Note: A potential spinoff of this option would be to include the EmailUser model (and anything else necessary to support it) in the django.contrib.auth app. In this case, we'd have to write a rule into our model detection that basically says that if a model is subclasses AbstractBaseUser, include it in the app if and only if it is the value of the AUTH_USER_MODEL setting. This addresses the second "disadvantage" listed below, but adds more code.

Advantages

  • Simple implementation, a developer must only include auth_email in INSTALLED_APPS and set AUTH_USER_MODEL = 'auth_email.User'.
  • Principle of least surprise: auth_email.User should behave exactly as auth.User would, with the exception of no username.
  • A small amount of new code means fewer opportunities to introduce new bugs.
  • Makes few assumptions about how an email user should behave, which leaves further implementation up to each use case

Problems

  • django.contrib.auth could potentially be modified a bit to reduce code duplication, specifically with regards to admin.UserAdmin. If there is interest in this I will modify my branch.
  • Requires installing an additional app besides django.contrib.auth.

Option 2: The authtools approach

Make auth code more generic, eliminating the need for a separate app. Optionally, provide an EmailUser for email as username.

  • Refactor the views and forms provided by django.contrib.auth to be generic.
  • Use swappable to provide a model in django.contrib.auth which uses email as username. (optional)

django-authtools could roughly be viewed as a patch. authtools was written to provide flexibility that the built in auth code didn't provide. Once the built-in auth code has this flexibility, authtools can go away. Refactoring forms and views in auth to be generic towards username in the same manner as authtools will allow auth to work with a broader range of custom user models. In this case, specifically, with a user model which uses email as username.

In regards to including a user model in django which uses email as username. Swappable already works with two concrete models with the same swappable value. Adding an EmailUser to django.contrib.auth.models that is also swappable on AUTH_USER_MODEL works as desired. The only model that is installed is the one referenced by settings.AUTH_USER_MODEL. All other models swappable on AUTH_USER_MODEL are ignored and not installed. authtools is actually already doing this. If you look at authtools.models.User https://github.com/fusionbox/django-authtools/blob/master/authtools/models.py#L86, this model is swappable on AUTH_USER_MODEL but will not be installed unless settings.AUTH_USER_MODEL points to it.

Putting EmailUser in auth.models should be considered independent of the views/forms refactor. This refactor will make it much easier for 3rd party apps to provide their own custom user models, and remove the necessary code duplication found in authtools.

Relevant Tickets

  • Ticket for refactoring django.contrib.auth.views to use class-based views - 17209
  • Ticket for refactoring django.contrib.auth.forms.UserCreationForm to work with custom user models. - 19353

Much of the code for the refactoring work has already been written, but for some reason the tickets stagnated and never fully made it into master.

Advantages

  • using EmailUser only requires pointing AUTH_USER_MODEL at 'auth.EmailUser'
  • built in auth code for forms and views becomes more generic and extensible.
  • auth code lives in a single code base, rather than two contrib apps.
  • no extra contrib application or confusion surrounding whether you should use views/forms form auth or auth_email.

Disadvantages

  • Now we have EmailUser included in auth. What about FacebookUser, or PhoneNumberUser. (note that this is in reference to adding EmailUser, not to refactoring auth.forms and auth.views to be more generic).
  • Swapped-out models are always loaded into the app cache. This could cause overhead to load models that are never used.

Final decision

To be determined.