DRF authentication with custom user models

DRF authentication with custom user models

I decided to write this article because of the challenges I faced when working with DRF authentication especially with a custom user model. I was frustrated at some point but with the help of googling, stack overflow and other friends who helped me through it, then I thought to myself no one should go through what I went through. Authentication with django rest framework (DRF) can be tricky but not to worry that's the reason for this article. Let's dive in.

We would need to create our working directory and install our packages there, it's best practice to always do this in a virtual environment.

virtualenv drf-auth

This should spin up your virtual environment for you. Next thing to do is to activate it

Windows --> drf-auth\Scripts\activate 
Linux --> source drf-auth/bin/activate

Let's install the libraries we would be working with

pip install djangorestframework, django

Now that we have that covered let's head on over to our terminal and start setting up our project.Firestly we need to create create a folder where our django project would be located, mkdir authentication, enter the directory using cd authentication in here we would need to create the basic structure of our django project with the django-admin startproject drf-auth. With this we have created our django project directory, now let's create our app that handles the authentication. python manage.py startapp auth. Let's start writing some code.

In our auth app let's open our models.py file and add these lines of code, I would explain later as we progress.

from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin

class UserManager(BaseUserManager):
    def create_user(self,username,  email, password=None):
        if username is None:
            raise ValueError("users should have a username")
        if email is None:
            raise ValueError("users should have an email")
        user = self.model(username=username, email=self.normalize_email(
            email))
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, username, email, password=None):
        if password is None:
            raise TypeError("Password should not be none")
        user = self.create_user(username, email, password)
        user.is_superuser = True
        user.is_staff = True
        user.is_active = True 
        user.save(using=self._db)
        return user

Let's go through what we have written above, first we created a class called UserManager this is the class that handles the logic of a custom user as you can we specified the basic parameters for authentication, if you wish you can add a new field either first_name or last_name . Note that any parameter you are adding, make sure you're also passing them in the methods so as not to run into errors. The create_user method does the user creation and the create_superuser creates a superuser. Remember all superusers have the superuser, staff and is_active fields set to True, always remember to return the user that is being saved to the db if not you would run into errors. Now to create the user model

class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=255, unique=True, db_index=True)
    email = models.EmailField(max_length=255, unique=True, db_index=True)
    is_verified = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username' ]

    objects = UserManager()

    def __str__(self):
        return self.email

This is just like creating a normal User model except you can see we have some parameters in the User class AbstractBaseUser and PermissionsMixinThe use of the PermissionsMixin is to gove our user model access to the inbuilt django permissions framework while the AbstractBaseUser is to show that this isn't a normal user django model but a custom user model. The USERNAME_FIELDS variable is to show refer the user email in the username field so that anywhere the user wants to input a username it passes it as the email. The REQUIRED_FIELDS variable shows the fields that should be required when creating superuser from the terminal when you run python manage,py createsuperuser.

We are done with writing the logic that creates a user and superuser and also the models that handles all that, now let's write the serializer that would serialize this data so as to make it available as a public API endpoint. We would need to create a file in our app folder called serializer.py you can do this easily from your terminal touch serializer.py exclusive only to Linux users :). Let's write code for the API endpoints now.

from rest_framework import serializers
from .models import User
from django.contrib.auth import authenticate


class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(
        max_length=64, min_length=6, write_only=True)
    class Meta:
        model = User
        fields = [
            "id",
            "username",
            "password",
            "email",
        ]

    def validate(self, attrs):
        email = attrs.get("email", "")
        username = attrs.get("username", "")

        return super().validate(attrs)

    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
        return user

class LoginSerializer(serializers.ModelSerializer):
    email = serializers.EmailField(max_length=255, min_length=4)
    password = serializers.CharField(
        max_length=68, min_length=6, write_only=True)
    username = serializers.CharField(
        max_length=255, min_length=3, read_only=True)

    class Meta:
        model = User
        fields = ["email", "password"]

    def validate(self, attrs):
        email = attrs["email"]
        password = attrs["password"]
        try:
            user = authenticate(email=email, password=password)

            if not user:
                raise AuthenticationFailed("Invalid credentials, try again")
            if not user.is_active:
                raise AuthenticationFailed(
                    "Your account is disabled contact admin")
            if not user.is_verified:
                raise AuthenticationFailed("Your account is  not verified")

            return {
                "email": user.email,
                "username": user.username,
            }
        return super().validate(attrs)

This might look strange to you of you're new in using DRF but that's np big deal serializers are like forms, they act like forms but instead of returning the values in html forms they return the data back in json format. What the RegisterSerializer does is just ti collect data and validate it, if the data is valid it creates a user, while the LoginSerializer. To see what we have written in action we need to create views to display what we have written. Move to your views.py file and write this code

from rest_framework import generics, status, views
from rest_framework.response import Response

class RegisterView(generics.CreateAPIView):
    serializer_class = RegisterSerializer

    def post(self, request, *args, **kwargs):
        seriaizer = self.serializer_class(data=request.data)
        if seriaizer.is_valid():
            seriaizer.save()
            return Response(seriaizer.data, status=status.HTTP_201_CREATED)
        return Response(seriaizer.errors, status=status.HTTP_400_BAD_REQUEST)

This handles the register serializer view, as you can see it takes a post request just like your normal django form does and it checks if the form is valid, if the form is valid it creates a user and returns a json format of the user or a serializer user data. Let's write the code for logging.

class LoginView(generics.GenericAPIView):
    serializer_class = LoginSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        return Response(user_serializer.data, status=status.HTTP_200_OK)

What this does it to check if this is information passed to the login serializer is valid, if it is valid it would authenticate the user then log them in. To finish it off let's create our urls so we can be able to navigate to the endpoints. In our app create a file called urls.py. In it add this code

from django.urls import path
from . import views

urlpatterns = [
    path('register', views.RegisterView.as_view()),
    path('login', views.LoginView.as_view()),
]

Now let's navigate to our project directory's urls.py file and add this lines of code to get everything perfectly.

path('auth/', include('auth.urls')),

Now we can be able to view our code and test it out, you can do that using postman or any other API testing app. If you want to know more about django authentication you can checkout their official documentation here. Cheers!!