Contents
Prerequisites
Before starting, you should have the following python dependencies installed:
Django==2.1
django-rest-framework==0.1.0
djangorestframework==3.8.2
gunicorn==19.9.0 # Our WSGI server
psycopg2==2.7.5 # Only if using Postgresql
PyJWT==1.6.4
pytz==2018.5
If you are using Docker, you can get up and running with the following Dockerfile:
FROM python:3.6-alpine
MAINTAINER Sebastian Ojeda <sebastian@oddjobbox.com>
WORKDIR /app
COPY requirements.txt .
RUN apk add --no-cache --virtual .build-deps
build-base postgresql-dev jpeg-dev zlib-dev
&& pip install -r requirements.txt
&& find /usr/local
( -type d -a -name test -o -name tests )
-o ( -type f -a -name '*.pyc' -o -name '*.pyo' )
-exec rm -rf '{}' +
&& runDeps="$( scanelf --needed --nobanner --recursive /usr/local | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' | sort -u | xargs -r apk info --installed | sort -u )"
&& apk add --virtual .rundeps $runDeps
&& apk del .build-deps
COPY . .
CMD ["python3", "manage.py", "runserver", "0:8000"]
Creating a User model
The Django user model is pretty straight forward. We will be inheriting from the AbstractBaseUser
and the PermissionsMixin
classes to create our model.
import jwt
from datetime import datetime
from datetime import timedelta
from django.conf import settings
from django.db import models
from django.core import validators
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
class User(PermissionsMixin, AbstractBaseUser):
"""
Defines our custom user class.
Username, email and password are required.
"""
username = models.CharField(db_index=True, max_length=255, unique=True)
email = models.EmailField(
validators=[validators.validate_email],
unique=True,
blank=False
)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
# The `USERNAME_FIELD` property tells us which field we will use to log in.
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ('username',)
# Tells Django that the UserManager class defined above should manage
# objects of this type.
objects = UserManager()
def __str__(self):
"""
Returns a string representation of this `User`.
This string is used when a `User` is printed in the console.
"""
return self.username
@property
def token(self):
"""
Allows us to get a user's token by calling `user.token` instead of
`user.generate_jwt_token().
The `@property` decorator above makes this possible. `token` is called
a "dynamic property".
"""
return self._generate_jwt_token()
def get_full_name(self):
"""
This method is required by Django for things like handling emails.
Typically this would be the user's first and last name. Since we do
not store the user's real name, we return their username instead.
"""
return self.username
def get_short_name(self):
"""
This method is required by Django for things like handling emails.
Typically, this would be the user's first name. Since we do not store
the user's real name, we return their username instead.
"""
return self.username
def _generate_jwt_token(self):
"""
Generates a JSON Web Token that stores this user's ID and has an expiry
date set to 60 days into the future.
"""
dt = datetime.now() + timedelta(days=60)
token = jwt.encode({
'id': self.pk,
'exp': int(dt.strftime('%s'))
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
As you can see, the token is created dynamically with the @property
decorator with an expiry of
60 days. The UserManager
that will be called when creating a new user is as follows:
from django.contrib.auth.models import BaseUserManager
class UserManager(BaseUserManager):
"""
Django requires that custom users define their own Manager class. By
inheriting from `BaseUserManager`, we get a lot of the same code used by
Django to create a `User`.
All we have to do is override the `create_user` function which we will use
to create `User` objects.
"""
def _create_user(self, username, email, password=None, **extra_fields):
if not username:
raise ValueError('The given username must be set')
if not email:
raise ValueError('The given email must be set')
email = self.normalize_email(email)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email, password=None, **extra_fields):
"""
Create and return a `User` with an email, username and password.
"""
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
"""
Create and return a `User` with superuser (admin) permissions.
"""
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(username, email, password, **extra_fields)
There is a lot of code that was dumped out just now but my goal is to get you up and running as
soon as possible. I recommend reading through the code line by line to make sure you understand
what is going on (this is generally a good idea whenever you are copying code from the
internet!). The User
class and UserManager
are all you need to create a custom user in
Django! Just don’t forget to let Django know that these models exist by declaring your app
in the settings.py
file:
INSTALLED_APPS = [
...
'rest_framework',
'authentication', # My'authentication` app
...
]
AUTH_USER_MODEL = 'authentication.User'
Authentication Backend
By default, Django does not know how to authenticate your JWTs. To fix this, we must the create
the following backends.py
file:
import jwt
from django.conf import settings
from rest_framework import authentication, exceptions
from .models import User
class JWTAuthentication(authentication.BaseAuthentication):
authentication_header_prefix = 'Bearer'
def authenticate(self, request):
"""
The `authenticate` method is called on every request regardless of
whether the endpoint requires authentication.
`authenticate` has two possible return values:
1) `None` - We return `None` if we do not wish to authenticate. Usually
this means we know authentication will fail. An example of
this is when the request does not include a token in the
headers.
2) `(user, token)` - We return a user/token combination when
authentication is successful.
If neither case is met, that means there's an error
and we do not return anything.
We simple raise the `AuthenticationFailed`
exception and let Django REST Framework
handle the rest.
"""
request.user = None
# `auth_header` should be an array with two elements: 1) the name of
# the authentication header (in this case, "Token") and 2) the JWT
# that we should authenticate against.
auth_header = authentication.get_authorization_header(request).split()
auth_header_prefix = self.authentication_header_prefix.lower()
if not auth_header:
return None
if len(auth_header) == 1:
# Invalid token header. No credentials provided. Do not attempt to
# authenticate.
return None
elif len(auth_header) > 2:
# Invalid token header. The Token string should not contain spaces.
# Do not attempt to authenticate.
return None
# The JWT library we're using can't handle the `byte` type, which is
# commonly used by standard libraries in Python 3. To get around this,
# we simply have to decode `prefix` and `token`. This does not make for
# clean code, but it is a good decision because we would get an error
# if we didn't decode these values.
prefix = auth_header[0].decode('utf-8')
token = auth_header[1].decode('utf-8')
if prefix.lower() != auth_header_prefix:
# The auth header prefix is not what we expected. Do not attempt to
# authenticate.
return None
# By now, we are sure there is a *chance* that authentication will
# succeed. We delegate the actual credentials authentication to the
# method below.
return self._authenticate_credentials(request, token)
def _authenticate_credentials(self, request, token):
"""
Try to authenticate the given credentials. If authentication is
successful, return the user and token. If not, throw an error.
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY)
except:
msg = 'Invalid authentication. Could not decode token.'
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get(pk=payload['id'])
except User.DoesNotExist:
msg = 'No user matching this token was found.'
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = 'This user has been deactivated.'
raise exceptions.AuthenticationFailed(msg)
return (user, token)
Again, this is a lot of code being thrown out, but I like to think that it is fairly straight forward if you have some experience with Python and Django.
We must also remember to update our settings.py
file to tell Django where to find our custom
authentication backend:
...
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'authentication.backends.JWTAuthentication',
)
}
...
By now you have created a custom User
model and UserManager
model, and created a custom
JWTAuthentication
class to authenticate your user tokens. The last piece missing is to setup your
user views for DRF to handle.
DRF Serializers
There are a couple of views that need to be serialized to finally get up and running. The first one
is the RegistrationSerializer
from rest_framework import serializers
from .models import User
class RegistrationSerializer(serializers.ModelSerializer):
"""
Creates a new user.
Email, username, and password are required.
Returns a JSON web token.
"""
# The password must be validated and should not be read by the client
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True,
)
# The client should not be able to send a token along with a registration
# request. Making `token` read-only handles that for us.
token = serializers.CharField(max_length=255, read_only=True)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
def create(self, validated_data):
return User.objects.create_user(**validated_data)
This serializer will receive a username, email, and password and will return a user token if
authentication is successful. Next we need a way of logging existing user in. We will create a
LoginSerializer
for this:
class LoginSerializer(serializers.Serializer):
"""
Authenticates an existing user.
Email and password are required.
Returns a JSON web token.
"""
email = serializers.EmailField(write_only=True)
password = serializers.CharField(max_length=128, write_only=True)
# Ignore these fields if they are included in the request.
username = serializers.CharField(max_length=255, read_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
"""
Validates user data.
"""
email = data.get('email', None)
password = data.get('password', None)
if email is None:
raise serializers.ValidationError(
'An email address is required to log in.'
)
if password is None:
raise serializers.ValidationError(
'A password is required to log in.'
)
user = authenticate(username=email, password=password)
if user is None:
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
)
return {
'token': user.token,
}
DRF Views
The login process will also return a user token, but only if the user has already been created.
With these two serializers in place, we can move on to our view.py
file. We simply need to
include a view for registering and for logging in.
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import User
from .serializers import LoginSerializer
from .serializers import RegistrationSerializer
class RegistrationAPIView(APIView):
"""
Registers a new user.
"""
permission_classes = [AllowAny]
serializer_class = RegistrationSerializer
def post(self, request):
"""
Creates a new User object.
Username, email, and password are required.
Returns a JSON web token.
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{
'token': serializer.data.get('token', None),
},
status=status.HTTP_201_CREATED,
)
class LoginAPIView(APIView):
"""
Logs in an existing user.
"""
permission_classes = [AllowAny]
serializer_class = LoginSerializer
def post(self, request):
"""
Checks is user exists.
Email and password are required.
Returns a JSON web token.
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
The final step is to set up our urls.py
file to map our views to a url.
from django.urls import re_path, include
from .views import RegistrationAPIView
from .views import LoginAPIView
urlpatterns = [
re_path(r'^registration/?$', RegistrationAPIView.as_view(), name='user_registration'),
re_path(r'^login/?$', LoginAPIView.as_view(), name='user_login'),
]
Summary
With all these files created, we are now able to register and log in users using our custom Django models and successfully authenticate our users with JSON Web Tokens. While most of this information has been dumped on this page, I hope it has been helpful to those looking to do something similar.