Custom users using Django REST framework
I started using Django about a year ago which is way later than it was first released. Back then, in 2005, the predominant user pattern consisted of username, email and password. As web development matured over the years we’ve seen this pattern getting simplified to just email and password. The built-in Django User model follows the old pattern and it can take a few extra steps to change it. It is recommended to roll your custom user model from the start of your project as replacing it in a later phase can be quite a hassle. Don’t just take my word for it, see the official recommendation. We will go over the steps necessary to get your customized User model set up so that an email address can be used as the primary identifier, RESTfully exposing his endpoints for client apps to use (ReactJS, iOS, Android and other).
Versions used: Python 3.6.9, Django 3.0, Django REST framework 3.10.3, Django Allauth 0.40.0, Django REST Auth 0.9.5
Project initialization
It’s always a good practice to create isolated Python environments for each project. Here I used Virtualenv for that.
Let`s get things going by quick starting a project named customuser
:
mkdir customuser && cd customuser
pip install django
pip install djangorestframework
django-admin startproject customuser .
Note that we do not apply any migrations just yet. Now go back to that official recommendation you didn’t bother clicking when reading the intro and find out why.
Third-party packages
Once the project is created we can begin installing third-party packages that will be working with our custom User model.
pip install django-allauth
Django Allauth handles user registration as well as social authentication. It is also good for email address verification, resetting passwords and much more.
pip install django-rest-auth
Django REST Auth conveniently provides API endpoints for user registration, login/logout, password change/reset, social auth, and more.
Make sure to update the INSTALLED APPS
list in "settings.py":
INSTALLED_APPS = [
...
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'django.contrib.sites',
'allauth',
'allauth.account',
'rest_auth.registration',
...
]
Users app
Now we can create a new app called users
which will be housing our custom User:
python manage.py startapp users
And add it to the INSTALLED_APPS
list:
INSTALLED_APPS = [
...
'users',
...
]
Custom User manager
The first thing to do is adding a custom Manager which subclasses BaseUserManager
and instructing it to use an email as the unique identifier instead of a username.
So create a “managers.py” file in the “users” directory:
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifiers
for authentication instead of usernames.
"""
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 Email must be set'))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
"""
Create and save a SuperUser with the given email and password.
"""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', 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)
Custom User model
There are two options when creating a custom User model: subclassing AbstractUser
or subclassing AbstractBaseUser
AbstractUser is a full User model, complete with fields, like an abstract class so that you can inherit from it and add your own profile fields and methods.
AbstractBaseUser only contains the authentication functionality, but no actual fields.
Here we will go with subclassing the AbstractUser
and making the following changes:
- Remove the
username
field - Make the
email
field required and unique - Set the
USERNAME_FIELD
which defines the unique identifier for theUser
model toemail
- Specify that all objects for the class come from the
CustomUserManager
we created prior - Add a couple of additional fields for good measure
Here is the code in “users/models.py”:
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import ugettext_lazy as _
from .managers import CustomUserManager
class CustomUser(AbstractUser):
username = None
email = models.EmailField(_('email address'), unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = CustomUserManager()
spouse_name = models.CharField(blank=True, max_length=100)
date_of_birth = models.DateField(blank=True, null=True)
def __str__(self):
return self.email
Settings
Adding the following line to the “settings.py” file will let Django know to use the new User class:
AUTH_USER_MODEL = 'users.CustomUser'
Next, we will add settings to configure django-allauth
. These are pretty much self-explanatory but you can find a full list with explanations here.
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/?verification=1'
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = '/?verification=1'
SITE_ID = 1
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
What we have set here:
- usage of email instead of username
- sending the email verification message upon user registration with verification link
- when user clicks on the link his email gets verified and is redirected to
/?verification=1
- outputting the email messages to console (to send actual emails you must set up Simple Mail Transfer Protocol, simplest way to do this is by using one of the SMTP service providers like Amazon SES)
After setting the AUTH_USER_MODEL
we can finally create and apply the migrations:
python manage.py makemigrations
python manage.py migrate
API
The last thing to do is to include allauth
and rest_auth
endpoints for dealing with our custom User in "urls.py":
from allauth.account.views import confirm_email
from django.conf.urls import url
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
url(r'^rest-auth/', include('rest_auth.urls')),
url(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
url(r'^account/', include('allauth.urls')),
url(r'^accounts-rest/registration/account-confirm-email/(?P<key>.+)/$', confirm_email, name='account_confirm_email'),
]
This will include:
User registration
POST /rest-auth/registration/
{
"email": "test@test.com",
"password1": "ujmik,ol.",
"password2": "ujmik,ol."
}
This will send out verification email with account confirmation URL looking like this: /accounts-rest/registration/account-confirm-email/MQ:1iU6Li:17ruSRLybL38zXvc91no26v2YGw/
User login
Login is denied before user confirms his email.
POST /rest-auth/login/
{
"email": "test@test.com",
"password": "ujmik,ol."
}
User logout
POST /rest-auth/logout/
Password change
User must be logged in to change password (duh…)
POST /rest-auth/password/change/
{
"new_password1": ".lo,kimju",
"new_password2": ".lo,kimju"
}
Currently logged in user
GET /rest-auth/user/
Custom User serializer
If you check the response when GETing the currently logged in user you will see something like this:
{
"pk": 1,
"username": null,
"email": "test@test.com",
"first_name": "",
"last_name": ""
}
As you can see the username field is still there and none of our additional fields are included. This happens because the default User serializer is still being used. A simple solution is to write our own and instruct rest_auth
to use it.
Create “serializers.py” in “users” directory:
from rest_framework import serializers
from users.models import CustomUser
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'email', 'first_name', 'last_name', 'spouse_name', 'date_of_birth']
And include the following in your “settings.py”:
REST_AUTH_SERIALIZERS = {
'USER_DETAILS_SERIALIZER': 'users.serializers.UserSerializer',
}
Conclusion
These are the basics for RESTfully creating and managing a custom User in Django REST framework. For more customization and options (like social authentication) take a deeper dive into django-allauth
and django-rest-auth
packages.
Originally published at https://krakensystems.co on January 7, 2020.