The Ultimate Tutorial for Django REST Framework: Login and Authentication (Part 2)

Dominik Kozaczko - Backend Engineer

Dominik Kozaczko

3 August 2018, 6 min read

thumbnail post

What's inside

In the previous article in this series, I showed you how to prepare an API that implements basic CRUD on objects quickly. This time, I'll show you how to log in to the API and how to regulate permissions.

Be sure to catch up with the work we’ve completed in other parts of the series:

We can distinguish two dominant groups among REST API use cases: (1) single-page applications (SPA) that take advantage of the browser's capabilities, and (2) mobile applications. In the case of the former, all we need is a standard session support mechanism provided by Django and supported by the DRF by default. Unfortunately, we can't use this mechanism in mobile applications where it's much more common to log in with a token: when running the application, we provide login details, the application connects to the API that generates the token, and the token is saved, so users don't have to remember the login and password - or have their device remember them and expose them to risk.

DRF provides a token authentication mechanism, and you can read about it in the documentation. This description is too detailed for our purposes (it's worth returning to it after the end of this series of articles).

That's why I'll be using djoser library.

$ pip install djoser

Let's start with the basic configuration:

settings.py

INSTALLED_APPS = [
    ...
    'djoser',
    'rest_framework.authtoken',
]

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
}

We can also add the token login to our URLs:

urls.py (global)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls)),
    path('api/auth/', include('djoser.urls.authtoken')),
]

To finish, we need to complete the migration related to tokens:

$ ./manage.py migrate

From this moment, we can get the login token with the help fo REST API:

$ curl -X POST -d '{"username": "admin","password": "top_secret"}' -H 
'Content-Type: application/json'  http://127.0.0.1:8000/api/auth/token/login/

You'll get something like this in response:

{"auth_token":"fe9a080cf91acb8ed1891e6548f2ace3c66a109f"} 

Let's secure our views from unauthorized access now

DRF offers several classes of permissions we can use to protect our API against unauthorized access.

The default permission is 'rest_framework.permissions.AllowAny', which - as the name suggests - allows everyone to do anything. Let's protect the API so that only logged-in users have access.

To do this, we need to modify settings.py by adding the following entry:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

Let's check whether our API is protected.

{"detail":"Authentication credentials were not provided."} 

That works! Without a token, all we got was an error. Let's use the token we got earlier.

$ curl -X GET http://127.0.0.1:8000/api/v1/friends/ -H 
'Authorization: Token fe9a080cf91acb8ed1891e6548f2ace3c66a109f' [{"id":1,"name":"John Doe"}] 

NOTE: For security reasons, it's critical that production API is only available through https.

However, setting a default class that manages permissions for the entire API isn't all there is. We can also set different ways to handle permissions individually for each ViewSet by setting the permission_classes attribute:

views.py

class MyViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.DjangoModelPermissions]

Have a look at the DRF documentation to learn more about default permissions classes.

Let's dive into the details now

Determining permissions is based on request analysis and returning bool value (True / False).

Now, let's follow the example I presented in the previous article where we create application that helps to manage the things we borrow to other people. Let's assume that the service for borrowing things was met with the interest of our friends who would like to use it as well. To manage that, we need to enable user registration. But first, we need to prepare permissions, so that only the owners of individual objects can modify them.

We can carry out user registration in many ways and I will leave out this stage to inspire you to figure it out on your own. It's a good idea to use djoser or rest_auth libraries here.

Let's add some information about the owner to our models. The most convenient way is creating a mixin or an abstract model:

models.py

class OwnedModel(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE)

    class Meta:
        abstract = True

class Belonging(OwnedModel):
    name = models.CharField(max_length=100)

# continue with other models

Remember to migrate the database afterwards:

$ ./manage.py makemigrations $ ./manage.py migrate 

It's time to prepare the permission checking class.

To implement it, we can use one of the following methods (or both of them): `has_permission` and` has_object_permission`. General permissions are always checked, and the object is only checked once the general permissions are accepted. These methods must return True if the permission has been granted, and False if it hasn't. The default value returned by both methods is True. If several permission validation classes are used in the view, all of them must pass successfully (the results are combined using AND).

Our view will check the permissions for an object, so we will implement the permissions as follows:

permissions.py

from rest_framework import permissions

class IsOwner(permissions.BasePermission):
    message = "Not an owner."

    def has_object_permission(self, request, view, obj):
        return request.user == obj.owner

The message attribute allows setting your own error message when you don't grant permission.

If we would like to allow any user to see our app's content, then the class would assume this form:

class IsOwner(permissions.BasePermission):
    message = "Not an owner."

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user == obj.owner

permissions.SAFE_METHODS contains a list of HTTP methods that don't write, i.e. GET, OPTION, and HEAD.

The next step is remembering the logged-in user as the owner of the newly created resource. DRF provides us with a useful method here (mind you, that method is included in the documentation under a completely different topic):

serializers.py

class FriendSerializer(serializers.ModelSerializer):
    owner = serializers.HiddenField(
        default=serializers.CurrentUserDefault()
    )

    class Meta:
        model = models.Friend
        fields = ('id', 'name')

# and so on for other serizalizers

To finish, let's use our new permissions class in ViewSets:

views.py

from .permissions import IsOwner

class FriendViewset(viewsets.ModelViewSet):
    queryset = models.Friend.objects.all()
    serializer_class = serializers.FriendSerializer
    permission_classes = [IsOwner]

# and so on with other ViewSets

Note that creating an object only checks the `has_permission` permission, so you may want to limit it to logged-in users. To do that, just import 'rest_framework.permissions.IsAuthenticated' and add it to the 'permission_classes` attribute. Remember - both must return True for permission to be granted.

DRF provides us with basic permission classes, and their names speak for themselves: AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly, IsAdminUser (the is_staff attribute is checked here, not is_superuser!), DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly, DjangoObjectPermissions.

The last three ones are based on Django and I think they're particularly interesting. While the first two implement standard model permissions, the last one requires the use of a library like django-guardian.

You can read more about this in the DRF documentation. Naturally, you can also use other libraries but I personally like DRY REST permissions because it follows the "fat models" principle recommended by Django creators.

In the next article in this series, I'll take a closer look at additional information in serializers, and in particular the dynamically-generated fields.

Dominik Kozaczko - Backend Engineer

Dominik Kozaczko

Backend Engineer

Dominik has been fascinated with computers throughout his entire life. His two passions are coding and teaching - he is a programmer AND a teacher. He specializes mostly in backend development and training junior devs. He chose to work with Sunscrapers because the company profoundly supports the open-source community. In his free time, Dominik is an avid gamer.

Tags

python
django
django rest framework

Share

Let's talk

Discover how software, data, and AI can accelerate your growth. Let's discuss your goals and find the best solutions to help you achieve them.

Hi there, we use cookies to provide you with an amazing experience on our site. If you continue without changing the settings, we’ll assume that you’re happy to receive all cookies on Sunscrapers website. You can change your cookie settings at any time.