Create a RESTFul API with Users, JWT Authentication and permission control using Django 1.11 & DRF — Part 2

Andreas Pogiatzis
7 min readJul 27, 2018

--

Source: Unsplash — Credit: @cdr6934

Brief catch up

In Part 1 of the tutorial we created a basic Django project with an extended user model and registered it on the admin panel for easy management. This is the Part 2 of the tutorial so in case you haven’t read the first part I would highly recommend it because this part builds on top of the previous work.

Adding DRF support

It’s time to REST! To get the RESTFul API up we initially need to install DRF in the virtual environment:

pip install djangorestframework==3.8.2

After pip is done with its magic, add the rest_framework app in the INSTALLED_APPS array of the config/settings.py file like this:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api',
'rest_framework', #Add this
]

Define serializers

Serializers are a core concept of DRF. To put the long story short, a serializer accepts querysets or Django model instances as input and converts them to python built in data types so that they can be easily converted to JSON/XML and visa versa.

Serialization/deserialization is the intermediate step before rendering/storing your data models so any logic that supplements inconsistencies of these two data states is placed here. For instance, if your Django model has 10 field in total but only 5 of them must be rendered as JSON through the API, then the model serializer is the perfect place to implement this.

Serializers are extremely customizable but DRF provides a couple of built in serializer classes that will get you out of the trouble many times. In this tutorial we will be using ModelSerializer and HyperlinkedModelSerializer. Simply put, ModelSerializer is used to serialize Django models. Alternatively, HyperlinkedModelSerializer provides the same functionality as Model serializer but instead of the primary key it displays a hyperlink to the model instance in the url field.

To define the required serializers, create a new file called serializers.py in the api folder and add the following code:

Alright there are some pretty interesting things going on here. Lets examine the code slowly.

UserProfileSerializer is straightforward. It’s a simple ModelSerializer to serialize the UserProfile model based on the fields specified in the fields tuple.

However UserSerializer is a bit more complicated. Because we need the UserProfile to be serialized/deserialized as part of the User model we created a Writable Nested Serializer as defined in the DRF documentation. That is, a serializer that uses another serializer for a particular field ( profile in this case). Recall that when defining the UserProfile model the related_name parameter was set in the OneToOne relationship definition. This is what enabled the use of the profile field in the UserSerializer class.

As a result, we created custom update and create methods to support the creation/update of the UserProfile object along with the creation/update the User object. I believe that the code in the create and update method is self explanatory so I won’t dive into explanations here. The only thing that needs to be highlighted here is the use of the set_password method. By using this method, the password is hashed and stored as a hash rather than plaintext which is a very important point in terms of security.

Note the the password field is set as a write_only field. Meaning that it will be used for deserialization(creating the model) but not for serialization.

Create viewset and define urls

Viewset is another core concept of DRF which is exactly what the name suggests, a set of views. It combines the logic for related views in a single class-based view. As such, instead of having http request methods like .get() or .post(), it provides actions such as .list() and .create() etc.

It also provides the convenience of creating url route configuration automatically for each action by using a Router class as you will see in a moment.

In our case we won’t be touching those actions since the default behavior of ModelViewSet fits our requirements. To do that add the following code in the api/views.py file:

Here, queryset field is just the set of objects that are subject to the view, and serializer field is simply the serializer class to be used for the model instances. Straightforward huh?

Let’s also setup the API endpoints for the user model. Create the file api/urls.py and type in:

The DefaultRouter class will define the standard REST (GET, POST, PUT, DELETE … ) endpoints for a resource as illustrated in the table below:

DefaultRouter routes — Source: DRF Documentation

It is also possible to define custom routers but this is out of the scope of this tutorial.

Finally, lets hook up the urls we just defined in the api application to the project under the api/ prefix by adding the code below in the config/urls.py file:

We have everything setup folks! Let’s run this and check what we have built! In the project directory again run:

python manage.py runserver

If everything went well, the server must be running on port 8000. Navigate to http://localhost:8000/api/users . You should be able to see the following interface:

This is DRF’s interface for interacting with the API. You will notice there is already one user there which is the superuser that we created earlier. The profile is reasonably null because the profile is not created when a user is created with the createsuperuser command which is okay.

The API interface allows to create, retrieve, update or delete users. So essentially, registering a new user can be done by sending a POST request to the user endpoint! All these are possible only from that little amount of code that we wrote! Amazing isn’t it?

Add JWT authentication

Now it’s time for the sparkly magic! Although this may sound like the most complicated task of all… it is actually the simplest. Bring it on!

Install dependencies

For adding authentication we will be using django-rest-auth and djangorestframework-jwt packages. Go ahead install the following dependencies to your environment:

pip install django-rest-auth
pip install django-rest-framework-jwt

Also add the rest_auth and rest_framework.authtoken app to your INSTALLED_APPS array:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api',
'rest_framework',
'rest_framework.authtoken', # Add this line
'rest_auth', # Add this line
]

Setup JWT Authentication

JWT authentication can be added by following the next two simple steps.

Add this snippet of code the bottom of your settings.py file:

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}

REST_USE_JWT = True

In addition add the line url(r’^auth/’, include(‘rest_auth.urls’)) to your api/urls.py file in the urlpatterns array so that it looks like this:

Finally, apply migrations for our newly installed apps and run the server!

python manage.py migrate
python manage.py runserver

By navigating to http://localhost:8000/api/auth/ you will notice that new endpoints are displayed after the changes. More particularly authentication endpoints. Feel free to try out your newly introduced login functionality by visiting http://localhost:8000/api/auth/login/ (You can ignore the username field). If you login successfully then you will receive a login JSON response with the JWT!

Just to give you a heads up that there is a little bit of redundancy in the response above, since the email, username and pk fields are actually sent along with the JWT payload, so you could retrieve them by Base64 decoding but still, this is cleaner for the tutorial

Setup view permissions

Everything is great so far…. but we missed something. All users can access other users’ profiles just by visiting their url this would have some serious privacy implications in a real world application. Considering that, we would desire to have the following permission model:

  1. Only admin users can get a list of all users
  2. Only admin can delete users
  3. Everyone can create a user (register)
  4. Logged in user can only retrieve/update his own profile.

All these can be enforces pretty easily using DRF’s permissions.To do that create a new file api/permissions.py and paste in the code below:

permissions.py

These permissions are self explanatory with regards to their restrictions. Just to node here that has_permission() refers to permission flag on a general level where the has_object_permission() refers to the permission of accessing a specific object. For instance the retrieve action (Get a single REST resource) would need the object permission rather than the general one.

Then, to apply the permissions on each of the actions of the viewset add the code annotated below to your api/views.py file so it looks exactly like the one below:

And…. that’s all! You are done! Run the server again and try out the results! I am pretty sure you are familiar with the runserver command by now…Try to get the list of all users by while logged in with a non admin user. You will notice that any unauthorized action will return a 403 status code.

Thanks for reading until this point, I hope that I was able to explain all the steps as clear as possible and also wish that you all enjoyed the tutorial! Again, the code is in my Github repo so if you didn’t feel like following along you can download it directly from there!

Till next time! ✌️

--

--

Andreas Pogiatzis
Andreas Pogiatzis

Written by Andreas Pogiatzis

☰ PhD Candidate @ UoG ● Combining Cyber Security with Data Science ● Writing to Understand

Responses (13)