Skip to main content

Django: Custom User model without username field (and using email in place of it)

Django by default uses the username field for authentication purposes of a user, but it's becoming a common trend to use email field for that purpose. As email identifies a user uniquely, the idea of an additional username field can be somewhat dropped. Also, the email can be used in the places where a username can be used, so it does not break any internal identification structure as well.

In the later sections, when I mentiond the (default) User model please think of the default Django User model (settings.AUTH_USER_MODEL).

So to make sure we're on the same page, our eventual goal is to get rid of the username field and use email field in the User model for authentication purposes.

Now, let's check out the usual ways to modify/extend the django User model:

  • Create a Profile model: For example a UserProfile model that has a one-to-one relationship with the User model. But this is to entend the functionalities of the User model e.g. to add a new field, not to modify it. So this option is of no use to us in this case.

  • Subclass AbstractUser (django.contrib.auth.models.AbstractUser): AbstractUser is an abstract model that can be subclassed directly to create a concrete User model. In fact, the default User model subclasses this. But again if we create a model subclassing this, the model would allow us to extend the functionalities of the abstract class, not to allow for dropping the username field. There is a possibility of overriding the username class variable (the field declaration) with e.g. None but that could break some other functionalities as this would make username a valid attribute of the model -- this could also lead to some hidden bugs down the line, so we want to omit it altogether. As a result, this option can't be taken into consideration as well.

  • Subclass AbstractBaseUser (django.contrib.auth.base_user.AbstractBaseUser): This is what we need (sorry, spoiler alert!). This is the superclass of AbstractUser abstract model as well and defines only 2 fields and associated logics. The fields it define are: password, last_login. So only a bare minimum and a perfect abstract superclass to extend and add only the fields we want.

So we'll be focusing on subclassing the AbstractBaseUser model and modify it to meet our need. The decision to take now is that which fields to include in our concrete user model as the AbstractBaseUser defines only 2 fields mentioned fields. From here, we can go for a total control over field definitions.


Before moving forward, if we were to extend the User model, the easier option is to create another profile model and put all the new fields and methods there. But this also has the penalty of creating an extra database table which can be avoided by using the AbstractUser model; as Abstractuser is an abstract model, it does not result in a database table itself. So we would get only one table created -- for the concrete subclass of it (which will contain all the additional fields).


Now, as our plan is to drop the username field and use email field as the USERNAME_FIELD, at this point we can get some inspiration from the AbstractUser model as it basically is what the default User model comprised of (as it does not override much). Here's the source of the default User model:

class User(AbstractUser):
    """
    Users within the Django authentication system are represented by this
    model.

    Username, password and email are required. Other fields are optional.
    """
    class Meta(AbstractUser.Meta):
        swappable = 'AUTH_USER_MODEL'

The fields AbstractUser defines are: username, first_name, last_name, email, is_staff, is_active, date_joined. It inherits from AbstractBaseUser so got password, last_login from there. It also has PermissionsMixin(django.contrib.auth.models.PermissionsMixin) as it's super class so it inherits is_superuser, groups, user_permissions fields from there. As you can imagine, the superclasses have all the relevant methods and attributes defined as well, and AbstractUser also implements some if it's own. The source codes of each are also linked so that you can check to have a better idea of whats's going on underneath.

So based on the above idea, a simple User model that does not have the username field and uses email as the username field can be defined as:

from django.db import models

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin

from django.utils.translation import ugettext_lazy as _


class User(AbstractBaseUser, PermissionsMixin):

    email = models.EmailField(unique=True, max_length=255, blank=False)

    # All these field declarations are copied as-is from `AbstractUser`
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(_('email address'), blank=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    # Add additional fields here if needed

    objects = UserManager()

    USERNAME_FIELD = 'email'

As seen, the major changes from default User model are:

  • email field is made unique and it must be provided
  • Declare email as the USERNAME_FIELD class attribute
  • A custom manager is needed that overrides the create_user and create_superuser methods used by Django internally in many places to create regular and super users, respectively.

Let's create our new UserManager now, this is also inspired from the idea implemented by the manager used in AbstractUser:

from django.contrib.auth.base_user import BaseUserManager


class UserManager(BaseUserManager):

    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        '''Create and save a user with the given email, and password.'''
        if not email:
            raise ValueError('The given email 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_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)

We need to move this definition before our User model or import if it's defined in some other module.

The major change from the default UserManager is the absense of username in the create_user and create_superuser methods. In our example, these methods delegate the actual user creation to the _create_user method which is set up accordingly to create users based on email-password.

Note that, the names User and UserManager are only a convention, you can give any name you want but it's highly recommended that you keep the same naming conventions so that it all makes sense to the next reader.

That's it! Now we only need to set our custom User model as the default user model in the settings file (settings.py):

AUTH_USER_MODEL = 'path.to.new.User'

And we're done! We can now use our custom user model like the way we want. We can customize it further to meet our need but here I've shown the basics of such an implementation.


Django source:

Comments

Comments powered by Disqus