Creating a Secure Django RestFUL API

Tafadzwa L Nyamukapa
13 min readJun 30, 2021

--

In this tutorial you are going to learn how you can create a secure Django API using djangorestframework and djangorestframework-simplejwt.

You are also going to learn how to write unit tests for your Django API using APITestCase.

Get Started

  • This tutorial is divided it into three sections:
  1. CREATING THE REST API
  2. SECURING THE API
  3. REST API UNIT TESTING

Checkout the you-tube video here

1. CREATING THE REST API

In this section i’m assuming you have your virtual environment setup and ready to go. If not; you can install virtualenv or alternatively pipenv unto you machine.

  • NB First make sure your virtual environment is activated

Install Django Framework

pip install django
  • Create Django Project
django-admin startproject secure_tesed_django_api
  • After the Django project has been created , you will need to install a couple of dependencies:
pip install djangorestframework
pip install djangorestframework-simplejwt

Now that we have our setup out of the way lets jump into the application. Navigate into your project root folder where there is the manage.py file.

We are going to create a simple CRUD business API, that will be used to manage a Customer Model.

  • Create New app called business
python manage.py startapp business

After creating the business app make sure to add the module in the settings.py file under installed app.

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'business', #new here
]

1a. Customer Model

Now its probably time to create our model. Navigate to the /business/models.py file create a Customer Class follows:

You will probably notice that we have an additional PublishedManager class, nothing to worry about :). Now Django by default uses a Manager with the name objects to every Django model class.

So if you want to create your own custom manager,you can archive this by extending the base Manager class and add the field in your model in this case the published field.

class PublishedManager(models.Manager):
def get_queryset(self):
return super(PublishedManager, self).get_queryset().filter(status='published')

So what this manager does is to simply run a query that returns the Model data(in our case Customers) where the status=published. Therefore when we query this model in the future we will be doing something like this:

customers = Customer.published.all()

Instead of:

customers = Customer.objects.filter(status='published')

1b Registering the Model in Django Admin

To register the Customer model in admin /business/admin.py:

Also note another trick that you can do to concat two fields in Django admin. You simply create a function and give an obj as a param. The obj will then be used to get the desired fields in this case name and last_name. Also note that in the list_display tuple we then use the name of the function in this case full_name.

1c CRUD API

Now that we have our Model Ready Lets create the CRUD API.

  • First create an api app
python manage.py startapp api
  • Add the api module in the settings.py file and also add the rest_framework that we installed earlier on.
...
...
'business',
'rest_framework', #new here
'api', #new here
]

1c.1 API URLS

  • Now include the api app in the base urls.py file where there is the settings.py file. /secure_test_django_api/urls.py
  • Finally create a urls.py file the api app and add the following routes /api/urls.py

In the above snippet we have created the customer routes with views CustomerView and CustomerDetailView which we are going to create shortly. The CustomerView is essentially going to handle our GET allrequest and save POST request then; The CustomerDetailView is going to handle our GET, PUT and DELETE requests.

1c2 API Views

Navigate to the views.py inside the app folder and add the following code /api/views.py.

Now Lets break this code down

  • CustomerView
    As mentioned above the CustomerView is going to handle our get all request and post request by extending the rest_framework APIView. Note how we are now making use of the Manager that we created in the Customer Model.
...
customers = Customer.published.all()
...

Here we are simply getting all Customers that has the status= published. Also note that we have a CustomerSerializer which we will create shortly with many=True meaning Serializer has to serialize the List of Objects.

  • CustomerDetailView
    Is going to handle the rest of our CRUD request operations, ie get, put and delete requests. Since we are going to perform a queryset that is going to get a specific Customer by the pk, for the rest of the requests; we need a way of handling the model DoesNotExist exception which is thrown if you query against a Customer that is not in the database.
  • There are multiple ways of handling such a scenario , like for instance one way would be to try except every request in this view, but the downfall with this approach is the bottleneck of unnecessary repetition of code.
  • To solve this we can make use of a python decorator pattern that lets you annotate every function request which will handle the model DoesNotExist exception. See code below:

The above snippet checks of a Model with a given pk exists, if it does it runs the request via

x = fun(*args, **kwargs)

Else if the resource does not exist it then returns a Not Found exception.

return Response({'message': 'Not Found'}, status=status.HTTP_204_NO_CONTENT)

A decorator is a python concept that lets you run a function within another function thus providing some abstraction in code that lets you use same code base in different scenarios or alter the behavior of a function or a class. Since we are going to pass a parameter in our decorate ie a Class Model we need some form of Factory Function that will take the param and later send it down the chain with other func params

Im not going to go in detail about python decorators as it is a wide concept.

1c3 API CustomerSerializer

Lets create CustomerSerializer class

  • Create a file called api/serializers.py in the api folder with the following code
  • To create a rest serializer class you need to extend the ModelSerializer base class.

1c4 Running app

From this stage everything should be fine now you can go a ahead and run your migrations.

  • Also remember to create a superuser
python manage.py makemigrations 
python manage.py migrate
python manage.py createsuperuser

You can even go ahead and test with postman to see if the application is behaving as expected.

  • Note we will write some unit tests in the last section of this tutorial so stay tuned :)

Examples:

POST request

2 SECURING THE API

In this section we are going to use djangorestframework and djangorestframework_simplejwt that we installed earlier to secure our end points.

Go to the api/views.py file and add the permission classes as below:

At this stage both of our views should be protected if you try to hit one of the end points eg get /api/customers/

You probably notice you are now getting a HTTP 403 Forbidden error, lets now implement the token authentication so that we will be able to hit our end points.

2a REST TokenAuthentication

The djangorestframework comes with a token based mechanism for authenticating authorising access to secured end points.

Lets start by adding a couple of configurations to the settings.py file.

  • Add rest_framework.authtoken to INSTALLED_APPS and TokenAuthentication to REST_FRAMEWORK.
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'business',
'rest_framework',
'api',
'rest_framework.authtoken', #new here
]
REST_FRAMEWORK = { #new here
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
...

After adding the configurations make sure you migrate to apply the authtoken tables.

python manage.py migrate

Generate Token

Now to make successful requests we need to pass an Authorization Header to our request with a Token.

  • To generate a token we need an account that we created earlier if not just quickly create one:
python manage.py createsuper --username admin --email tafadzwalnyamukapa@gmail.com
  • You can now generate your token by running the django drf_create_token command:

You should now be able to see a string like this

Generated token 73d29cb34e8a972741462fa3022935e43c18a247 for user admin
  • Now lets run the get /api/customers/ request that we tried earlier on with curl:
curl http://localhost:8000/api/customers/ -H 'Authorization: Token 73d29cb34e8a972741462fa3022935e43c18a247' | json_pp

Now we have successfully retrieved our list of customers.

  • NB Note that you need to pass the token with a Token value in the Authorization Header

Now this works just fine but if a client want to be able to get the token and run the exposed secured end points ,there should be a way of doing that. Not to worry djangorestframework comes with a helper end point that should let the client provide their credentials ie username and password and make a POST request in order to retrieve the token.

We are going to use obtain_auth_token view to archive the above scenario.

Client Requesting Token

Navigate to api/urls.py file and add the following route:

Now the client should be a be able to make a post request to /api/api-token-auth to obtain the Authorization token: Request:

curl -d "username=admin&password=admin" -X POST http://localhost:8000/api/api-token-auth/

Response:

{"token":"73d29cb34e8a972741462fa3022935e43c18a247"}
  • Now at this point the client has successfully obtained the token,its now up to them to save it in local Storage,or session Cookies or any other state manager, in order to make subsequent requests.

2b JWT Authorization and Information Exchange

  • Up to this point we have been using the REST Token for,authorization, while this works fine, there is another excellent way that we can use to archive this that is more secure when transmitting information between two parties ie JWT (Json Web Token).
  • This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JSON Web Token structure?

  • The JWT consists of a Header.Payload.Signature in that order.
xxxx.yyyy.zzzz

Simple JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU

Header

  • The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.
{
"alg": "HS256",
"typ": "JWT"
}

Payload

  • The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.
"token_type": "access",
"exp": 1543828431,
"jti": "7f5997b7150d46579dc2b49167097e7b",
"user_id": 5

Signature

  • To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
  • To get started with djangorestframework-simplejwt

Navigate to settings.py, add rest_framework_simplejwt.authentication.JWTAuthentication to the list of authentication classes:

  • Also, in api/urls.py file include routes for Simple JWT’s TokenObtainPairView and TokenRefreshView views:

Obtain JWT Token

  • To obtain the token you need to make a POST request to the /api/token/ end point on the TokenObtainPairView: Request:
curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "admin123#"}' http://localhost:8000/api/token/

Response:

{
"access" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjI0NzAxMzY3LCJqdGkiOiIwNmMyNjU0NjQyOWU0MThkODUzYzljZDViOTUyYmYyZSIsInVzZXJfaWQiOjF9.nW_bq87ob0PT5vm8uQ4ZsczO5jIZxtD6XTb1vQdz7_w",
"refresh" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyNDc4NzI2MywianRpIjoiM2Q0NzdhZmZiOGFhNDRhZjkzMmJhZDI0NjlhNmYwZWYiLCJ1c2VyX2lkIjoxfQ.s4rOL75ddLGCFnLt38Kwa3Du1O-j5Z7YC0cx0aetW4Q"
}
  • You can notice that in our response we got the access and refresh tokens.
  • We are going to access the secured endpoint eg /api/customers/ by using the access token.
curl http://localhost:8000/api/customers/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjI0NzAxMzY3LCJqdGkiOiIwNmMyNjU0NjQyOWU0MThkODUzYzljZDViOTUyYmYyZSIsInVzZXJfaWQiOjF9.nW_bq87ob0PT5vm8uQ4ZsczO5jIZxtD6XTb1vQdz7_w' | json_pp
  • NB Notice that we have used Bearer keyword instead of Token in our request Authorization Header.
  • The access token by default is valid for 5 minutes . After the expiry time has elapsed you cant use the same token else you will get an “Token is invalid or expired”.
{
"code" : "token_not_valid",
"messages" : [
{
"token_class" : "AccessToken",
"token_type" : "access",
"message" : "Token is invalid or expired"
}
],
"detail" : "Given token not valid for any token type"
}
  • To get a new access token you need to make a POST request with refresh token as data /api/token/refresh/, TokenRefreshView
  • The refresh token by default is valid for 24HRS.
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyNDc4NzI2MywianRpIjoiM2Q0NzdhZmZiOGFhNDRhZjkzMmJhZDI0NjlhNmYwZWYiLCJ1c2VyX2lkIjoxfQ.s4rOL75ddLGCFnLt38Kwa3Du1O-j5Z7YC0cx0aetW4Q"}' \
http://localhost:8000/api/token/refresh/
  • To configure token behavior eg lifetime, add the SIMPLE_JWT configurations in settings.py:

3. REST API UNIT TESTING

  • In this section we are going to look at how to run unit test on django restful api.
  • We are going to use the restframework APITestCase and the APIClient for our CRUD requests.
  • To begin delete the /api/tests.py file in the api folder.
rm /api/tests.py
  • Created a new folder inside the /api folder called tests
mkdir /api/tests
  • Navigate to the newly created tests folder.
cd /api/tests/
  • Create a test file test_customer_api.py
cat test_customer_api.py
  • Create an __init__.py inside the same /tests folder so that the django test runner will be able to pick our test file.
cat __init__.py
  • Now add the following snippet inside the newly created test_customer_api.py file.
  • Before we jump to the restframework APITestCase lets start with this django urls unit testing.
  • The above code simply tests the get /api/customers/ url to see if it is firing the correct ViewClass.
  • The default behavior of the test utility is to find all the test cases (that is, subclasses of unittest.TestCase) in any file whose name begins with test

Here we are using the django reverse function to get the absolute url of the path. We then assert the resolved url function view_class name against the name of the Class View that we want it to trigger.

  • To run the test; run the test command (./manage.py test ) with the name of app. If you dont add the name of the app the test runner will look for all apps in the project and run the test cases if there are any.
python manage.py test apiSystem check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.004s
OK

The test successfully runs 1 test and it passed.

  • Now lets continue with djangorestframework tests.

APITestCase

The REST framework uses includes test case classes , that mirror the existing Django test cases classes. They include:

  • APISimpleTestCase
  • APITransactionTestCase
  • APITestCase
  • APILiveServerTestCase

Alright now the code has extended a bit, but nothing to worry about :), lets break it down.

CustomerAPIViewTests

Now in this case we want to test the CustomerView that does our get all and post request.

  • Since we are now working with the database we need to extend an appropriate test case in our case an APITestCase.
class CustomerAPIViewTests(APITestCase):
  • We then use the django reverse function to get the absolute url by name “customer”.
customers_url = reverse("customer")
  • The setUp method will only be run once on each test case.
def setUp(self):
self.user = User.objects.create_user(
username='admin', password='admin')
self.token = Token.objects.create(user=self.user)
#self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
  • Inside the function we are creating the user, and the token. The django test runner sets up a volatile DB on every run and tears it down after each test case, and with that in mind we don’t necessarily need a tearDown function.
  • The APITestCase comes with a client from the APIClient class which we then use to configure our request Headers using the credentials method, so we don’t need to explicitly declare the client.
  • Setup the request Authorization Header with a token to the client:
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
  • Here we are making a get customers request to the /api/customers/ endpoint.
response = self.client.get(self.customers_url)
  • We are using assertEqual to make our declarative statements, for our test.
self.assertEqual(response.status_code, status.HTTP_200_OK
  • Here we are declaring /asserting that the response’s status_code == 200.
  • Now the above test case was for an authenticated client , now lets test an un-authenticated client and see if we attain the desired results.
self.client.force_authenticate(user=None, token=None)
  • To bypass authentication entirely for this test case we use the force_authenticate function on our client and assign the user or token to None.
  • We then assert a 401 response on the status_code.
  • The post request , we are using the same client to make a post request, and assert a 201 status Created.

CustomerDetailAPIViewTests

In this test class we are doing almost everything that we have covered above expect for a few things. Lets take a look

customer_url = reverse('customer-detail', args=[1])

You will probably notice that our reverse function is now a bit different , this is because the customer-detail path take an argument pk See urls.py code below:

...
path('customers/<int:pk>/', api_views.CustomerDetailView.as_view(), name="customer-detail"),
...

So we then pass an argument in the args list, in this case we are passing a 1.

  • Since we want to perform get, delete,put requests we need to pre-insert a customer in the DB during the setUp , since the customer is going to be used by the rest of the test cases.
  • Also note that this is now a different TestClass so we need to create a user , token and pass the Authorization token to the client request Header. Pre-Inserting Customer
self.client.post(self.customers_url, data, format='json')

GET and DELETE requests

...
def test_get_customer_autheticated(self):
response = self.client.get(self.customer_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['name'], 'Johnson')
def test_get_customer_un_authenticated(self):
self.client.force_authenticate(user=None, token=None)
response = self.client.get(self.customer_url)
self.assertEqual(response.status_code, 401)
def test_delete_customer_authenticated(self):
response = self.client.delete(self.customer_url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
...

In the get by id /api/customer/1 request , we are asserting that the response has a name == ‘Johnson’:

self.assertEqual(response.data['name'], 'Johnson')

In the delete request /api/customers/1 ,we are asserting that the response will return a 204 No content Found status_code.

  • Now Finally Run your tests:
python manage.py test apiCreating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 1.447s
OK
Destroying test database for alias 'default'...
  • If you have 7 passed tests then CONGRATULATIONS you have created your REST API UNIT TESTING.
  • NB As a rule of thumb make sure you mess around with the assertions, just to make sure your tests are working as Expected.

END !!

  • If there is anything you feel i should have covered or improve ,Please let me know in the comments section below.

Thank you for taking your time in reading this article.

KINDLY FORK AND STAR THE REPO TO SUPPORT THIS PROJECT :)

Source Code Git repo

The source code of this repo

Pull Requests

I Welcome and i encourage all Pull Requests….

Created and Maintained by

License

MIT LicenseCopyright (c) 2021 Tafadzwa Lameck Nyamukapa

--

--

Tafadzwa L Nyamukapa

Fullstack developer with a huge passion for building software, and explore new technologies. #Springboot #Django #nodejs #flutter #reactjs #laravelphp #express