Create a RESTFul API with Users, JWT Authentication and permission control using Django 1.11 & DRF — Part 2
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:
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
username
andpk
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:
- Only admin users can get a list of all users
- Only admin can delete users
- Everyone can create a user (register)
- 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:
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! ✌️