Multi-Role User Authentication in Django Rest Framework
Tags: django, djangorestframework, python, multiroleuserauthentication
Published: 2023-07-12 16:17:12

## Introduction:
User authentication is a fundamental aspect of many web applications. Django provides a powerful authentication system out-of-the-box, but sometimes you need to extend it to support multiple user roles. In this post, we'll explore how to implement multi-role user authentication using Django Rest Framework (DRF). 



## Setting Up the Project:
Let's start by creating a new Django project. If you don't have a Django environment set up, you can create one by following these steps:

1. Create a virtual environment: `python -m venv venv`
2. Activate the virtual environment: `venv\Scripts\activate`

Once your environment is activated, install Django and Django Rest Framework:
```
pip install django djangorestframework
```
You can follow any approach you prefer to setup environment. When your env is ready, open terminal (with env activated) and run the following commands:
```bash
django-admin startproject multi_role_auth
cd multi_role_auth
```



### Start our `authentication` app:
Open terminal (with env activated) and run the following commands:
```bash
python manage.py startapp authentication
```

Now that we have our project structure ready, let's dive into the implementation.



## Defining User Model:
In the `authentication/models.py` file, we'll define a custom user model that extends the AbstractUser class from Django's authentication models. This model will include a `role` field to assign different roles to each user.
```py
# authentication/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from rest_framework.authtoken.models import Token


class User(AbstractUser):
    ROLE_CHOICES = (
        ('administrator', 'Administrator'),
        ('teacher', 'Teacher'),
        ('student', 'Student'),
        ('staff', 'Staff'),
    )

    role = models.CharField(max_length=15, choices=ROLE_CHOICES)
    
```

Feel free to customize the `ROLE_CHOICES` tuple to include the specific roles that are relevant to your application. Additionally, if you require more fields for the User model, you can easily add them to this model. You can refer to the [documentation](https://docs.djangoproject.com/en/4.2/ref/contrib/auth/) to explore all the default fields provided by Django's User model. This flexibility allows you to tailor the User model to meet the specific requirements of your project.


## Creating Serializers:
Next, create serializers for our authentication app. Serializers help in converting complex data types into JSON, making it easy to send data over HTTP.

Create `authentication/serializers.py` and add the following code:
```py
# authentication/serializers.py

from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username', 'email', 'role', 'password']
        extra_kwargs = {'password': {'write_only': True}}

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

Here, we've defined a `UserSerializer` that inherits from the `ModelSerializer` provided by DRF. We specify the model as our custom User model and define the fields to include in the serialized representation. Additionally, we set the "password" field as write-only to prevent it from being exposed in responses. In the `fields` attribute, you can include all fields by passing `fields = '__all__'`. 



## Creating Views:
Now, let's implement the views for user registration, login, and logout.

In `authentication/views.py`, add the following code:
```py
# authentication/views.py

from authentication.models import User
from authentication.serializers import UserSerializer
from django.contrib.auth import authenticate, login
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.permissions import IsAuthenticated


class UserRegistrationView(APIView):
    def post(self, request):
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)



class UserLoginView(ObtainAuthToken):
    def post(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')

        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            token, created = Token.objects.get_or_create(user=user)
            if created:
                token.delete()  # Delete the token if it was already created
                token = Token.objects.create(user=user)
            return Response({'token': token.key, 'username': user.username, 'role': user.role})
        else:
            return Response({'message': 'Invalid username or password'}, status=status.HTTP_401_UNAUTHORIZED)



class UserLogoutView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        print(request.headers) 
        token_key = request.auth.key
        token = Token.objects.get(key=token_key)
        token.delete()

        return Response({'detail': 'Successfully logged out.'})

```

In the `UserRegistrationView`, we handle the HTTP POST request for user registration. We validate the data using the `UserSerializer` and save the user if it's valid.

In the `UserLoginView`, we handle the user login functionality. We authenticate the user using the provided username and password, and if successful, generate a token using the Token model from DRF. We return the token along with the username and role in the response.

The `UserLogoutView` is responsible for logging out the authenticated user. It retrieves the token from the request's authentication header, deletes the token if it exists, and returns a success message.



## Updating URLs:
Finally, we need to define the URLs for our authentication app.

In `multi_role_auth/urls.py`, add the following code:
```py
# multi_role_auth/urls.py

from django.urls import path, include
from authentication.views import UserRegistrationView, UserLoginView, UserLogoutView

urlpatterns = [
    path('api/auth/register/', UserRegistrationView.as_view(), name='user-registration'),
    path('api/auth/login/', UserLoginView.as_view(), name='user-login'),
    path('api/auth/logout/', UserLogoutView.as_view(), name='user-logout'),
    # Add other URLs here
]

```

Here, we map 
`/api/auth/register/` URL to the `UserRegistrationView`, 
`/api/auth/login/` URL to the `UserLoginView`, and 
`api/auth/logout/` URL to the `UserLogoutView`. 



## Modifying `settings.py`:
To enable token-based authentication in DRF, we need to make some modifications to the settings.py file.

In "multi_role_auth/settings.py", add or update the following settings:
```py
# multi_role_auth/settings.py

# ...

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'rest_framework.authtoken',
    'authentication',
]

# ...

AUTH_USER_MODEL = 'authentication.User'

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}
```

Here, we've added `rest_framework` and `authentication` to the `INSTALLED_APPS` list to include the necessary packages. Additionally, we've configured the `DEFAULT_AUTHENTICATION_CLASSES` to use the `TokenAuthentication` class for token-based authentication. 

Remember to run migrations before testing the app:
```bash
python manage.py makemigrations
python manage.py migrate
```

### Test: 
Here I tested `authentication` endpoints using `Postman`

**Register a user:**
Send a POST request to `http://localhost:8000/api/auth/register/` with the following payload in the request body:
```
{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "password": "$tr0ngPa$$w0rd",
  "role": "student"
}
```
![Register a user by using postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u6o9jtvivodma0ld8lrf.png)

**Login:**
Send a POST request to `http://localhost:8000/api/auth/login/` with the following payload in the request body:
```
{
  "username": "johndoe",
  "password": "$tr0ngPa$$w0rd"
}
```
![Login by using postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xrqsy6kdcbkh24h5fcyv.png)

**Logout:**
Send a POST request to `http://localhost:8000/api/auth/logout/`  with the token in the request headers. Include an `Authorization` header with the value `Token {token}` (replace {token} with the actual token value obtained during login).
![Logout by using postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qbbdyzgn6as1xhrm0ss7.png)


## Use it in user-specific class(es)
Now that we have a User with the `role` field, we can use it in our user-specific classes. For example:
```py
# student/models.py  

from django.db import models
from authentication.models import User

class Student(models.Model):
    # other fields related to student ...
    student_id = models.CharField(max_length=10, unique=True)
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="student_account")
```
```py
# student/serializers.py  
# import ...
class StudentSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)

    class Meta:
        model = Student
        fields = '__all__'

    def create(self, validated_data):
        user_data = validated_data.pop('user')
        user = User.objects.create_user(**user_data)
        student = Student.objects.create(user=user, **validated_data)
        return student
```
Update views and URLs:
```py
# ...
class StudentRegistrationView(APIView):
    def post(self, request):
        serializer = StudentSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# Update Login view
class UserLoginView(ObtainAuthToken):
    def post(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')

        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            token, created = Token.objects.get_or_create(user=user)
            if created:
                token.delete()  # Delete the token if it was already created
                token = Token.objects.create(user=user)

            response_data = {
                'token': token.key,
                'username': user.username,
                'role': user.role,
            }

            if user.role == 'student':
                student = user.student_account  # Assuming the related name is "student_account"
                if student is not None:
                    # Add student data to the response data
                    student_data = StudentSerializer(student).data
                    response_data['data'] = student_data

            return Response(response_data)
        else:
            return Response({'message': 'Invalid username or password'}, status=status.HTTP_401_UNAUTHORIZED)
```
```py
# ... 
urlpatterns = [
    # ...
    path('api/auth/register/student/', StudentRegistrationView.as_view(), name='student-registration'),
    # ...
]

``` 
In case you created a new app for student, add it to `INSTALLED_APPS`.
Run **migrations**.

Here is a JSON data to register a Student:
```json
{
    "student_id": "1234567890",
    "user": {
        "username": "john_doe@stu",
        "email": "john.doe@test.com",
        "role": "student",
        "password": "secretpassword"
    }
}
```
And this is the login response:
```json
{
    "token": "bc2369f6cf4c7bf015c449773dc285e9e8c69caf",
    "username": "john_doe@stu",
    "role": "student",
    "data": {
        "id": 1,
        "user": {
            "username": "john_doe@stu",
            "email": "john.doe@test.com",
            "role": "student"
        },
        "student_id": "1234567890"
    }
}
```

## Conclusion:
In this tutorial, we've covered the process of implementing multi-role user authentication using Django Rest Framework. We defined a custom user model, created serializers for user registration and login, implemented views for user registration, login, and logout, and updated the project's URLs and settings to support token-based authentication. Furthermore, we have explored the detailed process of utilizing our customized User model in other specific models. 

By extending Django's built-in user model and utilizing the capabilities of Django Rest Framework, you can easily add role-based authentication to your Django applications. This allows you to differentiate user permissions and provide tailored experiences based on each user's role.

Feel free to explore further and add additional functionalities, such as password reset and role-based access control (RBAC), based on your application requirements.

Happy coding!