Sign up with your email address to be the first to know about new products, VIP offers, blog features & more.

Creating a Rest API with Django Rest Framework, OAuth2 with a Custom UserModel

When you know that your API will be used by third part apps, it is reasonable to prepare an authorization server using OAuth2 Framework.

 

Installing Django

Common preparation steps: A folder and a Virtual Environment

mkdir rest_api && cd rest_api
curl "https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore" >> ./.gitignore
git init && git add . && git commit -m 'initial commit'
virtualenv -p python3 venv && source venv/bin/activate

Install Django

pip install django

 

Create a Django Project using django-admin  (a collection of settings for an instance of Django, including database configuration, Django-specific options and application-specific settings):

django-admin.py startproject rest_api_project . && cd rest_api_project

 

Start a Django App using django-admin 

django-admin.py startapp rest_api && cd ..

startapp creates a Django app directory structure for the given app name in the current directory or the given destination.

 

Add the newly created app in INSTALLED_APPS on project’s settings.py file

INSTALLED_APPS = [
    #...
    'rest_api_project.rest_api'
]

 

Remove SQLite from DATABASES on project’s settings.py file. Add your own MySQL Db.

DATABASES = {
 'default': {
 'ENGINE': 'django.db.backends.mysql',
 'NAME': 'dbname',
 'USER': 'user',
 'PASSWORD': 'pass',
 'HOST': '127.0.0.1', # Or an IP Address that your DB is hosted on
 'PORT': '',
 },
}

Having a database like this is important because Django gives for free an Admin interface, in that interface you can manage your Models and Applications that you will enable to use your OAuth2 structure that you are creating here. You also will be able to manage tokens issued for users.

 

Install mysqlclient

pip install mysqlclient

 

Creating a CustomUser Model

This steps will enables us to create our own CustomUser as recommended here: Using a custom user model in Django

from django.contrib.auth.models import AbstractBaseUser, AbstractUser, BaseUserManager
from django.db import models 
from datetime import datetime
from django.utils import timezone

class CustomUserManager(BaseUserManager):
 def create_user(self, email, date_of_birth, gender, device, password=None):
 """
 Creates and saves a User with the given email, date of
 birth and password.
 """
 if not email:
 raise ValueError('Users must have an email address')

 user = self.model(
 email=self.normalize_email(email),
 date_of_birth = date_of_birth,
 gender=gender,
 device=device
 )

 user.set_password(password)
 user.save(using=self._db)
 return user

 def create_superuser(self, email, date_of_birth, gender='', device='', password=None):
 """
 Creates and saves a superuser with the given email, date of
 birth and password.
 """
 user = self.create_user(
 email,
 password=password,
 date_of_birth=date_of_birth,
 gender=gender,
 device=device
 )
 user.is_admin = True
 user.save(using=self._db)
 return user


class CustomUser(AbstractBaseUser):
 device = models.CharField(max_length=150, blank=True)
 gender = models.CharField(max_length=1, blank=True)
 username = models.CharField(
 max_length=150,
 unique=False,
 # validators=[username_validator],
 # error_messages={
 # 'unique': _("A user with that username already exists."),
 # },
 )

 email = models.EmailField(
 verbose_name='email address',
 max_length=255,
 unique=True,
 )
 date_of_birth = models.DateField()
 is_active = models.BooleanField(default=True)
 is_admin = models.BooleanField(default=False)
 is_superuser = models.BooleanField(default=False)
 first_name = models.CharField(max_length=30, blank=True)
 last_name = models.CharField(max_length=150, blank=True)
 date_joined = models.DateTimeField(default=timezone.now)

 objects = CustomUserManager()

 USERNAME_FIELD = 'email'
 REQUIRED_FIELDS = ['date_of_birth', 'gender']

 def __str__(self):
 return self.email

 def has_perm(self, perm, obj=None):
 "Does the user have a specific permission?"
 # Simplest possible answer: Yes, always
 return True

 def has_module_perms(self, app_label):
 "Does the user have permissions to view the app `app_label`?"
 # Simplest possible answer: Yes, always
 return True

 @property
 def is_staff(self):
 "Is the user a member of staff?"
 # Simplest possible answer: All admins are staff
 return self.is_admin

 

Inform Django of this custom Model. Add this setting on settings.py

AUTH_USER_MODEL = 'rest_api.CustomUser'

 

Make this App Migrations

python manage.py makemigrations rest_api

 

Migrate models

python manage.py migrate

If everything goes well, you should see something like this in your console:

Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions

Running migrations:
Applying contenttypes.0001_initial… OK
Applying auth.0001_initial… OK
Applying admin.0001_initial… OK
Applying admin.0002_logentry_remove_auto_add… OK
Applying admin.0003_logentry_add_action_flag_choices… OK
Applying contenttypes.0002_remove_content_type_name… OK
Applying auth.0002_alter_permission_name_max_length… OK
Applying auth.0003_alter_user_email_max_length… OK
Applying auth.0004_alter_user_username_opts… OK
Applying auth.0005_alter_user_last_login_null… OK
Applying auth.0006_require_contenttypes_0002… OK
Applying auth.0007_alter_validators_add_error_messages… OK
Applying auth.0008_alter_user_username_max_length… OK
Applying auth.0009_alter_user_last_name_max_length… OK
Applying auth.0010_alter_group_name_max_length… OK
Applying auth.0011_update_proxy_permissions… OK
Applying sessions.0001_initial… OK

 

Setup Django’s Admin Interface access.

python manage.py createsuperuser

Set your username, e-mail, password.

 

Test your setup now

python manage.py runserver

 

Access the Admin Interface

http://localhost:8000/admin

 

Install djangorestframework, django-oauth-toolkit and django-cors-middleware

pip install djangorestframework django-oauth-toolkit django-cors-middleware

 

Add them in INSTALLED_APPS on project’s settings.py file

INSTALLED_APPS = [
    #...
    'rest_framework',
    'oauth2_provider',
    'corsheaders'
]

 

Run migrations for corsheaders (django-oauth-toolkit), oauth2_provider (django-cors-middleware)

python manage.py migrate

 

Edit the rest_api_project.urls.py and add the highlighted lines:

from django.conf.urls import url, include

urlpatterns = [
    path('admin/', admin.site.urls)),
    url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]

 

On project’s settings.py add CorsMiddleware in MIDDLEWARE. It should be placed as high as possible, especially before any middleware that can generate responses such as Django’s CommonMiddleware or Whitenoise’s WhiteNoiseMiddleware. If it is not before, it will not be able to add the CORS headers to these responses.

MIDDLEWARE = (
    # ...
    'corsheaders.middleware.CorsMiddleware',
    # ...
)

 

Allow CORS requests from all domains (just for the scope of this tutorial):

#settings.py
CORS_ORIGIN_ALLOW_ALL = True

 

Execute the server and login into the Admin and create an Application

python manage.py runserver

Access: http://localhost:8000/admin/login/?next=/admin/

Under DJANGO OAUTH TOOLKIT click on Applications and Add Application. Set Client Type to Confidential, Authorization grant type to Resource owner password based and give a name to this application that will be our Oauth’s client.

 

Define the default authentication classes on settings.py. Do this only if you want to apply it globally. To apply protection per view, add a permission_classes attribute in each class. Thats what i’m going to do, so i commented this setting.

# REST_FRAMEWORK = {
#     'DEFAULT_AUTHENTICATION_CLASSES': (
#         'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
#     ),
#     'DEFAULT_PERMISSION_CLASSES': (
#         'rest_framework.permissions.IsAuthenticated',
#     )
# }

 

Create your custom views

# rest_api/views.py

from rest_framework.views import APIView, Response

class CustomView(APIView):
    def get(self, request, format=None):
        return Response("Some Get Response")

    def post(self, request, format=None):
        return Response("Some Post Response")

 

Create your list of urls for rest_api app – create this urls.py file inside rest_api_project.rest_api

touch rest_api_project/rest_api/urls.py
from django.conf.urls import url
from rest_framework import routers
from rest_api_project.rest_api.views import CustomView
router = routers.DefaultRouter()

urlpatterns = [
    url(r'customview', CustomView.as_view()),
]

urlpatterns += router.urls

 

Edit rest_api_project.urls to add the child urls highlighted

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

from django.conf import settings
from django.conf.urls import url, include

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    url(r'^api/', include('rest_api_project.rest_api.urls')),
]

 

Protecting Views

Add imports on this newly created views

from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope, OAuth2Authentication 
from rest_framework import viewsets, permissions

And for each viewClass add protection

# require token authentication
authentication_classes = [OAuth2Authentication]

# control permissions 
permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]

Test your setup now

python manage.py runserver

If everything is ok for now, you should see this result in your terminal:

Django version 3.0, using settings ‘rest_api_project.settings’
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Preparing Apache Server on Ubuntu Server

Connect to your server using ssh
 sudo apt update

 

Install OS requirements
sudo apt install apache2-dev python3-dev python3-pip libssl-dev mysql-server libmysqlclient-dev virtualenv

 

Prepare Apache
sudo apt-get install apache2
sudo chown ubuntu:root /var/www/ && cd /var/www

 

Install and configure MySQL server
sudo apt install libssl-dev mysql-server
Secure the installation executing:
sudo mysql_secure_installation #define what you need
Access the service and create a database, change authentication type and flush (the former 2 are needed to execute migrations)
sudo mysql -u root -p
CREATE DATABASE rest_api;
use mysql;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'newpassword';  
FLUSH PRIVILEGES;
exit;

 

Finish Apache setup for this project
Configure Virtual Hosts for the API
https://www.digitalocean.com/community/tutorials/how-to-set-up-apache-virtual-hosts-on-ubuntu-16-04
sudo nano /etc/apache2/sites-available/rest-api.conf
Paste this code:
<VirtualHost *:80>

ServerAdmin admin@rest_api
DocumentRoot /var/www/rest_api
ServerName rest_api

Alias /static /var/www/rest_api/static
<Directory /var/www/rest_api/static>
Require all granted
</Directory>

# https://www.digitalocean.com/community/tutorials/how-to-serve-django-applications-with-apache-and-mod_wsgi-on-ubuntu-14-04
# Configure mod_wsgi for Django step 1
<Directory /var/www/rest_api/project>
<Files wsgi.py>
Require all granted
</Files>
</Directory>

# Configure mod_wsgi for Django step 2
WSGIDaemonProcess api python-home=/var/www/rest_api/venv python-path=/var/www/rest_api
WSGIProcessGroup api
WSGIScriptAlias / /var/www/rest_api/rest_api_project/wsgi.py
WSGIPassAuthorization On


ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

 

Enable Mod_Rewrite

sudo a2enmod rewrite && sudo systemctl restart apache2

 

Prevent ModuleNotFoung wsgi error:
https://stackoverflow.com/questions/43330231/500-internal-server-error-mod-wsgi-apache-importerror-no-module-named-django/43354916#43354916
wget https://codeload.github.com/GrahamDumpleton/mod_wsgi/tar.gz/4.6.5 # or the version you want
tar -xvf 4.6.5 && cd mod_wsgi-4.6.5
./configure --with-python=/usr/bin/python3 --with-apxs=/usr/bin/apxs
sudo make && sudo make install
cd .. && sudo rm -rf mod_wsgi-4.6.5 && sudo rm -rf 4.6.5
Enable the new compiled model to be loded by apache
sudo nano /etc/apache2/mods-available/wsgi.load

Paste this into the file:

LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so

Enable the module

sudo a2enmod wsgi

Enable new site

sudo a2ensite rest-api

 

Disable default Apache site

sudo a2dissite 000-default.conf

 

Disable default Apache site

sudo systemctl restart apache2

 

Clone your project into this folder

git clone https://fernandorodriguespro@bitbucket.org/fernandorodriguespro/rest_api.git && cd rest_api

 

Create the venv on the server

rm -rf venv # remove if you left it on git 
virtualenv -p /usr/bin/python3 venv/
source venv/bin/activate

 

Install Python required modules

pip install  django djangorestframework django-oauth-toolkit django-cors-middleware mysqlclient

 

Edit ALLOWED_HOSTS variable of settings.py

ALLOWED_HOSTS = ['*']

Edit DATABASES variable of settings.py

 

Set DEBUG variable of settings.py to FALSE

 

Set STATIC FILES  on settings.py

mkdir static
STATIC_URL = '/static/' 
STATIC_ROOT = os.path.join(BASE_DIR, 'static') 
STATICFILES_DIRS = [         
    os.path.join(BASE_DIR, 'static'), 
]
python manage.py collectstatic
sudo systemctl restart apache2

 

Execute migrations

python manage.py migrate

 

Setup Django’s Admin Interface access.

python manage.py createsuperuser
sudo systemctl restart apache2

 

References

The main doc for this installation process can be found here: Django OAuth Toolkit for Django Rest Framework.

https://docs.djangoproject.com/en/2.2/ref/django-admin/

https://www.django-rest-framework.org/api-guide/

1 Response
  • SAntiago
    April 10, 2020

    Your post i excellent project, thanks for your cooperation, could you sen me your project please? my mail is santiagomollinedo1@gmail.com thanks a lot

What do you think?

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