How to build Django Rest API with OAuth 2.0

Django is an open-source Python web framework. It follows the model-template-views architectural pattern. If you are familiar with some other framework like Laravel you would find this tutorial simple enough to follow. It is important to note that Django views are similar in function to Laravel controller and Django templates will be similar to Laravel blade files. It goes without saying that these are just rough comparisons for the sake of understanding only.

The tutorial is divided broadly into three sections. If you want you can simply click and skip to the required section.

In this tutorial, we will learn how to set up and host a Django project and then build Restful API using Django and securing them with OAuth 2.0. We are using it because OAuth 2.0 is the modern standard for securing access to APIs.

Section A: Setting up a server for Django development

We will set up Django in Ubuntu 20.04. This section of the tutorial is kept minimal. If you are interested in an in-depth tutorial you can click here.

We will use Django with Python 3 so let us install the following packages. Open up your terminal and run the following commands. This will install all the necessary files for your project to run.

$ sudo apt-get update
$ sudo apt-get install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx

Next, Start PostgreSQL in your terminal and create a new Database called djangorest” for your Django project in PostgreSQL (Note: All password are kept simple for this tutorial. Make sure to create a strong password in a production environment.)

$ sudo -u postgres psql
postgres=# CREATE DATABASE djangorest;
postgres=# CREATE USER djangorestuser WITH PASSWORD 'password';
postgres=# ALTER ROLE djangorestuser SET client_encoding TO 'utf8';
postgres=# ALTER ROLE djangorestuser SET default_transaction_isolation TO 'read committed';
postgres=# ALTER ROLE djangorestuser SET timezone TO 'UTC';
postgres=# GRANT ALL PRIVILEGES ON DATABASE djangorest TO djangorestuser;
postgres=# \q

We will now install Python virtual environment called virtualenv. Virtual environments are a great way to isolate and maintain your own projects without interfering with any other projects in the server. In your terminal run the following commands.

$ sudo -H pip3 install --upgrade pip
$ sudo -H pip3 install virtualenv

Let us make a new project directory called djangorest” and enter it.

$ mkdir ~/djangorest
$ cd ~/djangorest

Inside the project directory create a Python virtual environment called “djangorestenv”

$ virtualenv djangorestenv

Now, activate the virtual environment.

$ source djangorestenv/bin/activate

Your prompt will change to indicate that you are now operating within a Python virtual environment. It will look similar to this: (djangorestenv)[email protected]:~/djangorest$

With your virtual environment active, install Django, Gunicorn, and the psycopg2(PostgreSQL adaptor) with the local instance of pip. Inside any virtual environment irrespective of the Python version, we always use pip.

(djangorestenv) $ pip install django gunicorn psycopg2

Now, let us create a new Django project called “djangorest” and a Django app called “restapi”.

(djangorestenv) $ django-admin.py startproject djangorest ~/djangorest
(djangorestenv) $ cd ~/djangorest
(djangorestenv) $ django-admin.py startapp restapi

We need to adjust our project settings now.

(djangorestenv) $ nano ~/djangorest/djangorest/settings.py

In ALLOWED_HOSTS add your server IP address or the domain of the application. In my case my IP was “192.168.198.140”

# ~/djangorest/djangorest/settings.py
ALLOWED_HOSTS = ['your_server_domain_or_IP', 'second_domain_or_IP', . . .]

Add import os on top of the file. We will need it for using our static files.

# ~/djangorest/djangorest/settings.py
import os
from pathlib import Path

In DATABASES keep the following config.

# ~/djangorest/djangorest/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'djangorest',
        'USER': 'djangorestuser',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '',
    }
}

Add a STATIC_ROOT for static files. We added import os before for this step.

# ~/djangorest/djangorest/settings.py
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

If you were editing the setting with the nano editor. Save the file by pressing ctrl + o and exit the file by pressing ctrl + x.

Let us now migrate the tables to our database.

(djangorestenv) $ ~/djangorest/manage.py makemigrations
(djangorestenv) $ ~/djangorest/manage.py migrate

Next, create an administrator (Super User) for your Django project. This user can log into the admin panel of your project. You will have to select a username, provide an email address, and choose and confirm a password.

(djangorestenv) $ ~/djangorest/manage.py createsuperuser

We can collect all of the static content like the css and js files into the directory location we configured in settings.py by typing:

(djangorestenv) $ ~/djangorest/manage.py collectstatic

Finally, exit the virtual environment by typing:

(djangorestenv) $ deactivate

We will now use Gunicorn as our web server gateway. The Gunicorn “Green Unicorn” is a Python Web Server Gateway Interface HTTP server. Create a Gunicorn systemd service file (You will need sudo privilege for this operation) with the command mentioned below. Our application is named “djangorest-gunicorn”.

$ sudo nano /etc/systemd/system/djangorest-gunicorn.service

Copy & Paste the content of this file from below. Feel free to make changes according to your system.

[Unit]
Description=djangorest-gunicorn daemon
After=network.target

[Service]
User=saif
Group=www-data
WorkingDirectory=/home/saif/djangorest
ExecStart=/home/saif/djangorest/djangorestenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/saif/djangorest/djangorest.sock djangorest.wsgi:application

[Install]
WantedBy=multi-user.target

Start the “djangorest-gunicorn” service and enable it for auto start on boot ups.

$ sudo systemctl start djangorest-gunicorn
$ sudo systemctl enable djangorest-gunicorn

Next, check the status of your “djangorest-gunicorn” service with the command:

$ sudo systemctl status djangorest-gunicorn

If you face any issue with your “djangorest-gunicorn” service make sure check the process logs by typing:

$ sudo journalctl -u djangorest-gunicorn

Important: Whenever you make changes in your project make sure to restart djangorest-gunicorn everytime

$ sudo systemctl restart djangorest-gunicorn

Configure Nginx to proxy_pass to “djangorest-gunicorn”. We are basically reverse proxying all request to the server from Nginx to “djangorest-gunicorn

$ sudo nano /etc/nginx/sites-available/djangorest
server {
    listen 80;
    server_name server_domain_or_IP;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/saif/djangorest;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/saif/djangorest/djangorest.sock;
    }
}

Save and close the file when you are finished. Now, we can enable the file by linking it to the sites-enabled directory:

$ sudo ln -s /etc/nginx/sites-available/djangorest /etc/nginx/sites-enabled

Test your Nginx for any error:

$ sudo nginx -t

Make sure to add a new rule in ufw to allow Nginx by typing:

$ sudo ufw allow 'Nginx Full'

Finally, If everything is fine. Simply restart your Nginx

$ sudo systemctl restart nginx

That’s it. Your server is now successfully configured to run Django projects. In my Nginx setting, I am listening to port 8005 and my Ip is 192.168.198.140. Therefore to view my Django project in the browser I need to type http://192.168.198.140:8005.

Section B: Build Django Model, Register with Admin panel and Perform CRUD Operations

Now that our initial setup is done and we have also created an app called “restapi“. Let us start building our Rest API.

Inside your project folder open “djangorest” and modify admin/ to accounts/. Your ~/djangorest/djangorest/urls.py should look like this:

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('accounts/', admin.site.urls),
]

Register your “restapi” app in ~/djangorest/djangorest/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restapi'
]

Create a new model called Quote in ~/djangorest/restapi/models.py

# models.py
from django.db import models

# Create your models here.

class Quote(models.Model):
    author = models.CharField(max_length=50)
    quote = models.CharField(max_length=200)
    def __str__(self):
        return self.author

Register Quote with the admin site in ~/djangorest/restapi/admin.py

from django.contrib import admin
from .models import Quote
# Register your models here.
@admin.register(Quote)
class QuoteAdmin(admin.ModelAdmin):
	list_display = ("author", "quote")

Migrate the Quote table (after activating virtual environment)

(djangorestenv) $ ~/djangorest/manage.py makemigrations
(djangorestenv) $ ~/djangorest/manage.py migrate

Now, insert some quote in the admin panel. Navigate to your domain/accounts (eg: http://192.168.198.140/accounts) and log in with your given credentials. Add some data in the Quote table as shown in Fig 1.

Fig 1: Sample Data in Quote Table

Section C: Integrate Rest API with OAuth 2.0

Now that our model is created and we can perform CRUD(Create, Retrieve, Update & Delete) operations on it from the admin panel. We can now install two new packages for generating Rest API and protecting it with OAuth and then migrate the tables. (Inside Virtual Environment)

(djangorestenv) $ pip install django-oauth-toolkit djangorestframework
(djangorestenv) $ ~/djangorest/manage.py makemigrations
(djangorestenv) $ ~/djangorest/manage.py migrate

Update ~/djangorest/djangorest/settings.py to modify INSTALLED_APPS. Add the two new packages.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restapi',
    'oauth2_provider',
    'rest_framework',
]

Also add the following lines at the bottom of ~/djangorest/djangorest/settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
    )
}

Create a new file serializers.py inside “restapi” and open and edit ~/djangorest/restapi/serializers.py

from rest_framework import serializers

from .models import Quote

class QuoteSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Quote
        fields = ('author', 'quote')

In ~/djangorest/restapi/views.py add a new class as shown below:

from rest_framework import viewsets,generics, permissions, serializers
from restapi.models import *
from .serializers import *

from django.contrib import admin
admin.autodiscover()
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope


class QuoteViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]
    queryset = Quote.objects.all().order_by('id')
    serializer_class = QuoteSerializer

In ~/djangorest/djangorest/urls.py add a new paths and also import include from django.urls. New urls.py should look like this

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('accounts/', admin.site.urls),
    path('', include('restapi.urls')),
    path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]

Now, create a new file urls.py in “restapi” and open ~/djangorest/restapi/urls.py to edit its content as shown below:

from django.urls import include, path
from rest_framework import routers
from . import views

router = routers.DefaultRouter()
router.register(r'quotes', views.QuoteViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

Next, we will check if our API is accessible. (Note: I am hosting my site in 192.168.198.140 and my port is 8005)

Let us now open Postman to view the API by typing the URL http://192.168.198.140:8005/quotes/ we get a 401 Unauthorized response as shown in Fig 2. This is good because it means that our API is protected from unauthorized access.

Fig 2: 401 Unauthorized Response.

To get a proper response we need to first generate a token. We can do it easily from the admin panel. First, create an application under the section “Django OAuth Toolkit”. Here is my config of the first application.

Client ID: wqmlu2vtNFYTv9motTkbmfZDaWfRx4tdDwIWQumy
User: 1
Client Type: Confidential
Authorization Grant Type: Resource owner password-based
Client Secret: wXHmsiXff4CXAH8YRiojeJMMWRQe3CwIR1ztcUzLN2UctcUUrIJFDO0dhAGvtaV9JfFDHd2rdf0ltzs9yd2KFd0VQxrH11VovC5yBWENKD8aZnQjrPus4nJc8UL4ASBA
Name: My App

Let us create an access token now. Fig 3 shows how the access token was configured. The token has both read and write permission. 12345 is a bearer token. In production it’s important to have a refresh token also; a refresh token is used to generate a new token after the expiry of the current token.

Fig 3: Token Generation

Let us now access the API again with this new bearer token. The bearer token is set in Authorization as shown in Fig 4 with 200 OK response code.

Fig 4: 200 OK Response with bearer token 12345

Similarly, we can access other the rest of the APIs. The structure is shared below in Python requests format.

############################################
##############GET All Quotes################

import requests

url = "http://192.168.198.140:8005/quotes/"

payload={}
headers = {
  'Authorization': 'Bearer 12345'
}

response = requests.request("GET", url, headers=headers, data=payload)

print(response.text)
############################################

##############GET Single Quote##############

import requests

url = "http://192.168.198.140:8005/quotes/2"

payload={}
headers = {
  'Authorization': 'Bearer 12345'
}

response = requests.request("GET", url, headers=headers, data=payload)

print(response.text)
############################################
##############UPDATE Quote##################
import requests

url = "http://192.168.198.140:8005/quotes/2/"

payload='quote=test1&author=test2'
headers = {
  'Authorization': 'Bearer 12345',
  'Content-Type': 'application/x-www-form-urlencoded'
}

response = requests.request("PUT", url, headers=headers, data=payload)

print(response.text)

############################################
##############DELETE Quote##################
import requests

url = "http://192.168.198.140:8005/quotes/2/"

payload={}
headers = {
  'Authorization': 'Bearer 12345'
}

response = requests.request("DELETE", url, headers=headers, data=payload)

print(response.text)

############################################
##############CREATE QUOTE##################

import requests

url = "http://192.168.198.140:8005/quotes/"

payload='quote=New%20Quote&author=New%20Author'
headers = {
  'Authorization': 'Bearer 12345',
  'Content-Type': 'application/x-www-form-urlencoded'
}


############################################

With this our tutorials is complete. Now, you can build Rest API in Django with OAuth. If you want to read more about the OAuth package you can click here. I have also uploaded the source code in Github. Feel free to explore it.

Click anywhere to get to my Github page https://github.com/sa1if3/djangorest

Saifur Rahman

Saifur Rahman is a Full Stack Django and Laravel Developer. Additionally, he has spent a significant amount of time to learn and research in the domain of the Internet of Things (IoT). He loves to share his work and contribute to helping fellow developers. Saifur also runs the following websites and services - Pingsms.in and Techmion.com

View all posts by Saifur Rahman →

Leave a Reply

Your email address will not be published. Required fields are marked *