|Version 8 (modified by russellm, 6 months ago) (diff)|
Adding an Email-based authentication model
- Option 1: Simple auth.User analog
- Option 2: The authtools approach
- Final decision
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:
The end goal is to provide as a built-in feature of Django a User model that is an exact analog of the historical contrib.auth.User, but with email as the identifying field.
There are two major design questions that need to be answered:
The first question: where should this code live?
Option 1: A new contrib.auth.email_user app
What it says on the tin. We add a new contrib app to contain the new code. Users install this app and point at the model/forms it provides.
- Good separation of concerns. contrib.auth remains essentially unchanged, except for any refactoring of base classes needed to support the new app. Users who don't want EmailUser don't have to carry any extra load from model parsing, import or caching.
- Sets a good example for third-party apps wanting to provide their own pluggable apps for Users.
- Requires 2 settings to use the new model -- INSTALLED_APPS and AUTH_USER_MODEL. The counterargument to this is that users are used to installing apps to get access to models.
Option 2: Include EmailUser in contrib.auth
This requires the interpretation of Meta: swappable as "don't make this model visible unless the this model is referred to". This wasn't the original intended interpretation of Meta: swappable, but the implementation supports this usage. If swappable is used in this way, the model is still loaded, parsed, and put into AppCache, but it isn't synchronized to the database, and any attempt to issue a query or generate a FK to the model will raise an error.
- Single point of configuration -- the developer need only set AUTH_USER_MODEL.
- Overhead. Regardless of whether you want to use EmailUser, it must be loaded, parsed, and put in app cache.
- Arguably sets a bad example. Rather than the historical "one app, one concern", it encourages developers to put all possible models into a single app, and use swappable to prevent loading of a models that aren't needed at runtime.
- Depending on exact implementation, may require logic in views, forms, admin etc to control which code is active, depending on the active User model.
Option 1: Simple Subclass
Just rewrite the User model, but substitute email for username. Essentially copy/paste User, editing as required.
- Easy to do, easy to understand the end product.
- No facility for reuse.
- Sets a bad example for other developers by doing copy-paste.
Option 2: MOAR abstract base classes!
(RKM - I'm being facetious with these titles. Feel free to edit with something more meaningful :-)
A number of implementations have been provided, providing various combinations of the above design decisions. 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.
- 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
- 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.
- 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.
- 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.
- 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.
To be determined.