A user is a common entity in most of the projects and dealing with all the related function such as login, register, logout etc is a necessity in such projects. Today, we’ll learn how can we write such services as API endpoints when creating APIs via Django Rest Framework. This blog post assumes that you know how to set up the Django project and to include Django Rest Framework. If you are not aware of its specifics and terminologies, then I’ll suggest you to, go through its basics first.

Pre-requisites

Create the users app within your project. It’s always a good idea to create a separate app for the user, even though you are not overriding anything, it always helps, to keep it flexible for future changes. We can create the user app as

python manage.py startapp users

We’ll now be creating the user model.

# users/models.py from django.db import models from django.contrib.auth.models import AbstractUser class CustomUser(AbstractUser): username = None email = models.EmailField('email address', unique=True) first_name = models.CharField('First Name', max_length=255, blank=True, null=False) last_name = models.CharField('Last Name', max_length=255, blank=True, null=False) USERNAME_FIELD = 'email' def __str__(self): return f"{self.email} - {self.first_name} {self.last_name}"

I have created a custom user model, with email as the USERNAME_FIELD as I want to use the email as the only unique identifier. For more info. on extending your Django’s user model, you may refer here. However, you may keep the Django’s default user as well. It won’t matter in our case.

For API interaction, a user generally provides a token along with each request to API endpoints as a proof of identity. Hence, we’ll be leveraging DRF’s token generation functionality to include this feature. For that, we’ll need to do the following things:

Include rest_framework.authtoken within INSTALLED_APPS in your project’s setting.

within in your project’s setting. Define authentication classes within your project’s setting as REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.TokenAuthentication', ] }

Once we are done creating our user model and setting up pre-requisites for token authentication, run the following commands

python manage.py makemigrations python manage.py migrate

Since all the preparations are done, let’s start creating API endpoints.

Login

It always helps to keep a record of what do you expect in API request from the user and what do you intend to provide as the API response. So, let’s make it clear through the following structure

POST /api/auth/login Parameters Name | Data Type | Required | Default Value | Description ------------------|-----------|----------|----------------|-------------------- email | text | true | null | email of the user. password | text | true | null | password of the user. Request { "email": "hello@example.com", "password": "VerySafePassword0909" } Response Status: 200 OK { "id": 1, "first_name": "John", "last_name": "Howley", "email": "hello@example.com", "is_active": true, "is_staff": false, "is_superuser": false, "auth_token": " 34303fc8c5a686f2e21b89a3feff4763abab5f7e " }

Through the above structure, we can conclude that for a login endpoint, we expect two fields email and password . As a response, we provide all the basic user details along with a auth_token which would act as primary authentication for any user using our API.

Now, create the serializers to take input and providing output as per the above-defined structure. Let’s define serializers

# users/serializers.py from django.contrib.auth import get_user_model from rest_framework.authtoken.models import Token from rest_framework import serializers User = get_user_model() class UserLoginSerializer(serializers.Serializer): email = serializers.CharField(max_length=300, required=True) password = serializers.CharField(required=True, write_only=True class AuthUserSerializer(serializers.ModelSerializer): auth_token = serializers.SerializerMethodField() class Meta: model = User fields = ('id', 'email', 'first_name', 'last_name', 'is_active', 'is_staff') read_only_fields = ('id', 'is_active', 'is_staff') def get_auth_token(self, obj): token = Token.objects.create(user=obj) return token.key class EmptySerializer(serializers.Serializer): pass

The UserLoginSerializer is the serializer which we will use to validate our input and AuthUserSerializer will be used to provide the response. You might notice, that we have created a MethodField in it. A method field in DRF is a read-only field whose value can be generated dynamically through a method definition as get_<field_name> . We have used the DRF inbuilt token generation method to get the token for the provided user object. We have also defined an EmptySerializer , its use would make better sense later in the post. Keep reading for now. 🙂

The flow for login endpoint would be to get an email and password, authenticate the user based on the given credentials and provide the appropriate response based on authentication. To keep the logic clear within our views, we can create a helper function and call it within the view. I like this approach as it helps to keep the code structure clear. Let’s create a function in a utils file as

# users/utils.py from django.contrib.auth import authenticate from rest_framework import serializers def get_and_authenticate_user(email, password): user = authenticate(username=email, password=password) if user is None: raise serializers.ValidationError("Invalid username/password. Please try again!") return user

The authenticate method would return a user instance if a user with given credentials exists otherwise, return None.

For defining views, we’ll define our own actions for the endpoints as that would make better sense. For that, inheriting GenericAPIViewSet would be appropriate. However, when defining a viewset, we also need to define the attribute serializer_class within the viewset class, we create. But there will be different serializer classes for different actions. Hence, we need to find a way to dynamically select the serializer class based on the provided action. We can do that by overriding the get_serializer_class method within the views

# users/views.py from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.permissions import AllowAny from rest_framework.response import Response from . import serializers from .utils import get_and_authenticate_user User = get_user_model() class AuthViewSet(viewsets.GenericViewSet): permission_classes = [AllowAny, ] serializer_class = serializers.EmptySerializer serializer_classes = { 'login': serializers.UserLoginSerializer, } @action(methods=['POST', ], detail=False) def login(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = get_and_authenticate_user(**serializer.validated_data) data = serializers.AuthUserSerializer(user).data return Response(data=data, status=status.HTTP_200_OK) def get_serializer_class(self): if not isinstance(self.serializer_classes, dict): raise ImproperlyConfigured("serializer_classes should be a dict mapping.") if self.action in self.serializer_classes.keys(): return self.serializer_classes[self.action] return super().get_serializer_class()

We can break down the above piece of code in the following points:

Apart from serializer_class , we have defined another attribute serializer_classes , which is a dictionary, whose keys would be a string named same as the action we want and value would be the corresponding serializer class. This will help us in fetching serializer classes based on the action. For our login action, we have defined the ‘login’ as key and its serializer class as the key’s value.

, we have defined another attribute , which is a dictionary, whose keys would be a string named same as the action we want and value would be the corresponding serializer class. This will help us in fetching serializer classes based on the action. For our action, we have defined the ‘login’ as key and its serializer class as the key’s value. The get_seriaizer_class method is invoked by the get_serializer method, which expects a serializer class in return. The method definition is quite clear. It checks if serializer_classes is a dictionary or not. If it isn’t, then it raises the error otherwise, returns the class correspond to the action defined in the dictionary. The interesting part is if the action is not defined in the serializer_classes , it will then fall back to use the value for the serializer_class attribute.

method is invoked by the method, which expects a serializer class in return. The method definition is quite clear. It checks if is a dictionary or not. If it isn’t, then it raises the error otherwise, returns the class correspond to the action defined in the dictionary. The interesting part is if the action is not defined in the , it will then fall back to use the value for the attribute. We define the permission_classes as AllowAny , since anyone would be allowed to perform login functionality. Just to define the serializer_class attribute, we have assigned to it, an EmptySerializer which accepts nothing. That’s the main use of this serializer class.

as , since anyone would be allowed to perform login functionality. Just to define the attribute, we have assigned to it, an which accepts nothing. That’s the main use of this serializer class. The login action is quite simple to understand, it performs exactly what is written. It passes the input data to the serializer class which would be UserLoginSerializer , check if it is valid or not. It then gets the user object through the helper function we have created above and returns the serialized output data after passing it through the AuthUserSerializer .

Now, you can see the advantage of defining a separate utility method, as the view now looks more clear and compact.

Register

First, we’ll define the structure for request and response, for the register API endpoint.

Register POST /api/auth/register Parameters Name | Data Type | Required | Default Value | Description ------------------|-----------|----------|----------------|-------------------- email | text | true | null | email of the user. password | text | true | null | password of the user. first_name | text | false | "" | first name of the user. last_name | text | false | "" | last name of the user. Request { "email": "hello@example.com", "password": "VerySafePassword0909", "first_name": "John", "last_name": "Howley", } Response Status: 201 Created { "id": 2 "first_name": "John", "last_name": "Howley", "email": "hello@example.com", "is_active": true, "is_staff": false, "is_superuser": false, "auth_token": " 34303fc8c5a686f2e21b89a3feff4763abab5f7e " }

We expect all the basic details from the user and returns a similar response as in case of login. Therefore, our output serializer can be reused. However, we would need to create a serializer for validating input. Let’s create one

# users/serializers.py from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.models import BaseUserManager User = get_user_model() ... class UserRegisterSerializer(serializers.ModelSerializer): """ A user serializer for registering the user """ class Meta: model = User fields = ('id', 'email', 'password', 'first_name', 'last_name') def validate_email(self, value): user = User.objects.filter(email=email) if user: raise serializers.ValidationError("Email is already taken") return BaseUserManager.normalize_email(value) def validate_password(self, value): password_validation.validate_password(value) return value

The above serializer inherits ModelSerializer , which allows it to use the model field’s definition. We validate the email to check if it already exists. In case, email exists, it raises the ValidationError otherwise, we return the normalized email value. The normalize_email method of BaseUserManager prevents different sign-ups of case sensitive email domains. For more info. on this, you may refer here.

Apart from it, we validate the password using Django’s password_validation method. To let you know, these validation methods must take the form validate_<field_name> for field-level validation.

The flow for register endpoint would be similar to that of login, except this time after serializer validation, we’ll create the user account and return the response. For creating a user account, we can define a helper function in utils. Write the following piece of code in utils.py

# users/utils.py ... def create_user_account(email, password, first_name="", last_name="", **extra_fields): user = get_user_model().objects.create_user( email=email, password=password, first_name=first_name, last_name=last_name, **extra_fields) return user

We can now create the register endpoint in views.py file

# users/views.py ... from .utils import get_and_authenticate_user, create_user_account from . import serializers class AuthViewSet(viewsets.GenericViewSet): permission_classes = [AllowAny, ] serializer_class = serializers.EmptySerializer serializer_classes = { 'login': serializers.UserLoginSerializer, 'register': serializers.UserRegisterSerializer } @action(methods=['POST', ], detail=False) def login(self, request): ... @action(methods=['POST', ], detail=False) def register(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = create_user_account(**serializer.validated_data) data = serializers.AuthUserSerializer(user).data return Response(data=data, status=status.HTTP_201_CREATED) def get_serializer_class(self): ...

Just below the login action, we have defined the action for register endpoint. We have added the serializer class for the register endpoint to serializer_classes dictionary in a similar fashion as in case of login.

Logout

For logout, we actually don’t want anything in the request. Hence there is no need for serializer in case of logout. We can simply define the action for logout in views.py as

# users/views.py from django.contrib.auth import get_user_model, logout ... class AuthViewSet(viewsets.GenericViewSet): permission_classes = [AllowAny, ] serializer_class = serializers.EmptySerializer serializer_classes = { 'login': serializers.UserLoginSerializer, 'register': serializers.UserRegisterSerializer, } @action(methods=['POST', ], detail=False) def login(self, request): ... @action(methods=['POST', ], detail=False) def register(self, request): ... @action(methods=['POST', ], detail=False) def logout(self, request): logout(request) data = {'success': 'Sucessfully logged out'} return Response(data=data, status=status.HTTP_200_OK) def get_serializer_class(self): ...

Since we don’t require any serializer for logout action, we haven’t defined any in serializer_classes dictionary. It will fall back to use the EmptySerializer which does not accept anything. We used Django’s logout method for logging out from the current session and provide a successful 200 OK response.

Change Password

Another most common utility is the API endpoint for changing password, providing you know your old password. For this as well, we’ll create a structure beforehand to define the request and response for getting the clarity.

Change password POST /api/auth/password_change (requires authentication) Parameters Name | Description -----------------|------------------------------------- current_password | Current password of the user. new_password | New password of the user. Request { "current_password": "NotSoSafePassword", "new_password": "VerySafePassword0909" } Response Status: 204 No-Content

As you may see from the above structure, we expect two fields in the API request. Hence, we’ll create a serializer for it. Let’s define the serializer in serializers.py

# users/serializers.py ... class PasswordChangeSerializer(serializers.Serializer): current_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True) def validate_current_password(self, value): if not self.context['request'].user.check_password(value): raise serializers.ValidationError('Current password does not match') return value def validate_new_password(self, value): password_validation.validate_password(value) return value

Apart from defining the serializer fields, we validate them as well. For current_password , we make sure that it is the current password of the user, accessed through context(this is passed automatically through the view) and has a check_password method associated with it. If it doesn’t match, then a ValidationError is raised. The new_password is validated via validate_password method which was also used above for register endpoint. We can now define the action for it within views.py as

# users/views.py from rest_framework.permissions import AllowAny, IsAuthenticated ... class AuthViewSet(viewsets.GenericViewSet): permission_classes = [AllowAny, ] serializer_class = serializers.EmptySerializer serializer_classes = { 'login': serializers.UserLoginSerializer, 'register': serializers.UserRegisterSerializer, 'password_change': serializers.PasswordChangeSerializer, } @action(methods=['POST', ], detail=False) def login(self, request): ... @action(methods=['POST', ], detail=False) def register(self, request): ... @action(methods=['POST', ], detail=False) def logout(self, request): ... @action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated, ]) def password_change(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) request.user.set_password(serializer.validated_data['new_password']) request.user.save() return Response(status=status.HTTP_204_NO_CONTENT) def get_serializer_class(self): ...

The password_change endpoint has the IsAuthenticated permission class defined on it as the user must be authenticated before accessing this endpoint. Once, the input data passes through the serializer validation, we set the new password for the user using set_password method.

That’s it, we have learned the implementation for the major auth operations. It is now time to create endpoints for them by registering into urls.py

# users/urls.py from rest_framework import routers from .views import AuthViewSet router = routers.DefaultRouter(trailing_slash=False) router.register('api/auth', AuthViewSet, basename='auth') urlpatterns = router.urls

Default router automatically creates the endpoints for the defined actions. The endpoint which would be generated as a part of this would be

/api/auth/login /api/auth/register /api/auth/logout /api/auth/password_change

Make sure to include these into your project level urls.py.

It’s done, you can now test these endpoints through DRF’s browsable interface.

References

Congrats! You made it through the last. Let’s meet in the next blog post.

Be curious and keep learning! 🙂