Building APIs is not a straightforward job. One has to not only write the business logic but also have a permission layer to protect an unauthenticated user from accessing APIs that are public.
Restricting unauthenticated access alone is not enough though, there are cases where a user need not be authenticated for read permissions but should not be given write permissions.
For instance, if I am building a website for cake recipes, I would want anybody to view all the recipes but only an authenticated user should be able to edit the recipes.
What we essentially need to accomplish is role based access. This is where Django Rest Framework shines.
Handling all of these cases should be a separate module that you can just plug in at the end once your business logic is complete.
Guess what? It is actually that simple if you use Django Rest Framework. So before we go any further please install DRF in your django app.
Authentication classes
As mentioned earlier, there are two distinct concepts that need to be applied here.
The first one being identifying a user that is making a request based on various authentication backends that can be configured in settings.py of your django project.
This can be done by using DEFAULT_AUTHENTICATION_CLASSES. You can configure this using just a couple of lines of code.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication'
)
}
This setting will ensure that if a API request contains a header called Authorization with a value that is equal to Token <auth_token>, request.user will automatically be set to the current user.
You could also use SessionAuthentication or any of the other authentication backends that are available that can be found here.
Use token based authentication
I usually prefer TokenAuthentication because the auth token is fixed and as long as it is present in the headers, one does not have to worry about saving anything in cookies and deal with making API calls to a domain that is different from the one where your frontend is hosted.
To generate an auth token for a Django user, simply include the below apps in your INSTALLED_APPS section and use this code to generate a token.
INSTALLED_APPS = [
'rest_framework',
'rest_framework.authtoken'
]
If you prefer not to apply the TokenAuthentication backend globally to all your APIs and only want to apply it to a few APIs, you could define a base view where the authentication class can be set explicitly and inherit this view wherever needed
from rest_framework.authentication import TokenAuthentication
from rest_framework.views import APIView
class BaseView(APIView):
authentication_classes = [
TokenAuthentication,
]
class MyAuthenticatedView(BaseView):
# This view will automatically apply TokenAuthentication
def get(self, request):
pass
def post(self, request):
pass
class MyNonAuthenticatedView(APIView):
# This view will not be authenticated
def get(self, request):
pass
def post(self, request):
pass
Permission classes
Now that we have taken care of authenticating a user, let us look at how we can perform role based access in the quickest possible way.
This can be achieved by using the DEFAULT_PERMISSION_CLASSES setting.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication'
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated'
)
}
This setting will ensure that any user who is authenticated has permission to use all your APIs. But this obviously won’t work in most cases since we discussed earlier that we would require role based access.
You can achieve this by explicitly defining permission classes in your base view or individual views just like the authentication classes that were defined above.
DRF provides a bunch of permission classes out of the box which can be found here. For the sake of an example let’s use the IsAuthenticatedOrReadOnly permission.
As the name suggests this permission allows an API to be accessed only if you are an authenticated user unless the API you are trying to access is read only in which case anybody can access it.
This solves the problem of the cake recipes example given earlier and can be translated to code in the following way.
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class RecipeViewSet(viewsets.ModelViewSet):
serializer_class = RecipeSerializer
permission_classes = (IsAuthenticatedOrReadOnly,)
def get_object(self):
return get_object_or_404(Recipe, id=self.request.query_params.get("id"))
def get_queryset(self):
return Recipe.objects.all()
If you try to access this API via the GET, HEAD, OPTIONS methods, you will get a response, but if you try to make a POST, PUT, PATCH call you will get a 403 forbidden response.
Custom permissions
In most cases, role based access would involve some specific business logic. For instance, you might have roles such as user, editor and admin.
To restrict access based on custom parameters like these. You can write a custom permission class by inheriting the BasePermission class and overriding the has_permission method.
from rest_framework.permissions import BasePermission
class IsAdmin(BasePermission):
def has_permission(self, request, view):
return request.user.is_admin
class IsEditor(BasePermission):
def has_permission(self, request, view):
return request.user.is_editor
class IsUser(BasePermission):
def has_permission(self, request, view):
return request.user.is_user
You could then chain these permissions together in views to determine which users will have access to which views.
from rest_framework.views import APIView
from .custom_permissions import IsAdmin, IsEditor, IsUser
# only admins are allowed to access this view
class APIView1(APIView):
permission_classes = (IsAdmin,)
# Admins or editors are allowed to access this view
class APIView2(APIView):
permission_classes = (IsAdmin | IsEditor,)
# all types of users are allowed to access this view
class APIView3(APIView):
permission_classes = (IsAdmin | IsEditor | IsUser,)
Closing notes
In conclusion, the quickest way to authenticate users and give them role based access is to use the TokenAuthentication class globally and then custom permission classes for your individual views.
Alternatively you could write a base view and give it some authentication classes as well as permission classes that cover most of your cases and inherit all your views from this base view.
I prefer the latter method as it gives me more control and I can choose not to inherit this base view in some cases where I want to set custom authentication and permissions.
Top comments (0)