What is Django?

Django has gained an enormous amount of popularity since its initial release in 2005. It is an open-source web framework written in Python. It is a high-level Python framework that helps in the speedy development of applications. It helps in creating the most realistic designs for applications. It is a professional tool to provide ease in web development. The essential qualities of the Django framework are the speed, security, and scalability of applications.

Bootstrap

Bootstrap is a famously known front-end framework that helps create and maintain websites and web applications. It is a popular CSS framework used in the development of creating responsive websites.  It helps in creating user-friendly interfaces with the help of CSS, JavaScript, and other relevant tools. The latest version of this framework is known as Bootstrap 4. It supports all major browsers but has a restriction towards Internet Explorer 9.

Django App Bootstrapping with Cognito

Using Bootstrap with Django

Bootstrap can help create user-friendly interfaces for Django applications. Bootstrapping a Django application written in a Python framework is a simple phenomenon. The steps include downloading the bootstrap from its official website, creating a static directory inside the application folder, putting the CSS and JS folders inside the static directory. After that, the next step is to create the template file, and then bootstrap is loaded. The last step is to link the files in code, and bootstrap will update the file.

What is Cognito?

Amazon provides its user identity and synchronization services through Cognito. AWS Cognito is a modern tool that provides a user identity management system to implement security and scalability in the application. With AWS Cognito, developers can build applications that provide exceptional user experience over multiple devices. Moreover, Cognito provides security by utilizing TLS (Transport Layer Security) and SSL (Secure Sockets Layer) in their forms for encryption. Also, the Microsoft Azure cloud platform hosts these forms, which provides security as well.

Why is AWS Cognito Needed?

As the technology is evolving, applications with Single Sign-On (SSO) services integration are gaining importance. As this technique already provides a predicted basic routing, its popularity leads to the speedy development of the application. The SSO technique helps authenticate users with a single username and a single password to access multiple applications and websites. To have easy access to features like sign in, register, forget password and reset, 2 step verification method, and many more on multiple sites, SSO proves helpful. The various vendors provide this service, such as Azure Active Directory B2C, Okta, Auth0, AWS Cognito, and many more. AWS Cognito is the cheapest solution provider for such purposes.

Using Cognito in Django App Bootstrapping

Amazon Cognito provides flexibility in usage and helps in the customization of the required workflow by the developer. The Cognito API returns id_token, access_token, and refresh_token after successful authentication. These tokens help in the identification of a specific user for a better user experience. Bootstrapping an application in Django using Cognito is a crucial process leading to an application’s better performance. It involves steps like

  • Installation of packages
  • Creating a user pool in AWS Cognito
  • Creating custom user model
  • Configuring REMOTE_USER
  • Configuring DRF
  • Configuring djangorestframework-jwt
  • Creating a test view
  • Running the server and making a request

Following is a scenario where we have a back-office user like an admin who deals with login and working with Django-admin and session authorization. Another user type is an application user who interacts with the API, registers in Cognito, and works with jwt-authorization.

Installation of Packages

Following is an example code for building flexible and extendable configurations using DRF (djangorestframework-jwt). The users can use the fork def-jwt library because the original library has maintenance issues. Pip is used to install the mentioned framework.

#installating packages
pip install djangorestframework cryptography drf-jwt

Creating a User Pool in AWS Cognito

The next step is to login into the AWS Console and goes straight to Cognito. Here the main thing to choose Manager User Pools and create a user pool to configure attributes. After that, a client for the frontend application is created by using Enable username-password (non-SRP) flow for app-based authentication (USER-PASSWORD-AUTH). Then the backend client is created by using Enable sign-in API for server-based authentication (ADMIN-NO-SRP-AUTH).

Testing Configuration

There is an easier way for testing integration by enabling hosted-UI and observing Sign-Up/Sign-In pages given by Cognito. JWT tokens are easily accessed after this and used in Postman for ensuring a proper configuration to Django. For this, Amazon Cognito Domain is in the Domain name section. For example:

https://domain_name.auth.eu-central-1.amazoncognito.com

Also, for the App client settings, there is a need to enable any OAuth Flows such as Implicit grant and the OAuth Scope such as openid. A call back URL is also available like the following:

http://localhost:4000/admin

After this, the users can access the login page via Launch Hosted UI. They can then create a user in the Users and Groups tab and use its credentials to log in. Finally, the browser will redirect to the callback URL, and it will provide id_token and access_token.

Creating Custom User Model

The best practice is to create a basic user model in Django Application development to save future usability. Using UUID will be a unique identifier provided for the user, which will help encrypt and provide flexibility in coding. Utilizing created-at and updated-at will help in tracking the user-customized information. Moreover, the function __ref__ helps collect any possible errors or exceptions in the code. Following is an example of creating a user model in Django.

import uuid
from django.db import models
class AbstractBaseModel(models.Model):
            uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
    created_at = models.DateTimeField('Created at', auto_now_add=True)
            updated_at = models.DateTimeField('Updated at', auto_now=True)
            class Meta:
            abstract = True
   def __repr__(self):
            return f'<{self.__class__.__name__} {self.uuid}>'

For creating a custom user account after the creation of the custom user model, the following example is to be taken in consideration:

from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator
from core.models import AbstractBaseModel
class User(PermissionsMixin, AbstractBaseUser, AbstractBaseModel):
            username_validator = UnicodeUsernameValidator()
            username = models.CharField('Username', max_length=255, unique=True, validators=[username_validator])
            is_active = models.BooleanField('Active', default=True)
            pass
            email = models.EmailField('Email address', blank=True)
    is_staff = models.BooleanField(
            'staff status',
            default=False,
            help_text='Designates whether the user can log into this admin site.'
            )
            USERNAME_FIELD = 'username'
            EMAIL_FIELD = 'email'
            REQUIRED_FIELDS = ['email']
            @property
            def is_django_user(self):
            return self.has_usable_password()

For the settings.py following code will change the default user model:

AUTH_USER_MODEL = ‘account.User’

Finally, the custom model is registered in admin.py by using the following piece of code:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, UsernameField
from django.utils.translation import ugettext_lazy as _
from account.models import User
class CustomUserCreationForm(UserCreationForm):
            class Meta(UserCreationForm.Meta):
            model = User
class CustomUserChangeForm(UserChangeForm):
            class Meta(UserCreationForm.Meta):
            model = User
            fields = '__all__'
            field_classes = {'username': UsernameField}
@admin.register(User)
class CustomUserAdmin(UserAdmin):
            fieldsets = (
            (None, {'fields': ('username', 'email', 'password', )}),
            (
            _('Permissions'),
            {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions', )}
            ),
            (_('Important dates'), {'fields': ('created_at', 'updated_at', )}),
            )
            readonly_fields = ('created_at', 'updated_at', )
            add_fieldsets = (
            (None, {
            'classes': ('wide', ),
            'fields': ('username', 'email', 'password1', 'password2', ),
            }),
            )
            form = CustomUserChangeForm
            add_form = CustomUserCreationForm
            list_display = ('username', 'is_staff', 'is_active', )

Configuring REMOTE_USER

If the external authentication resources are not available, the users can do the additional configuration manually. They can create a new user record in the database by using RemoteUserRecord functionality. Also, the users can create_unknown_user to change its behavior. Following is an example code for this:

MIDDLEWARE = [
            ...
            'django.contrib.auth.middleware.AuthenticationMiddleware',
            'django.contrib.auth.middleware.RemoteUserMiddleware',
                        ...
]
AUTHENTICATION_BACKENDS = [
            'django.contrib.auth.backends.RemoteUserBackend',
            'django.contrib.auth.backends.ModelBackend',
]

Configuring DRF

The default permission class in core/api/permissions is changed in order to prevent security breaches. Following is the new code that overrides the default one:

class DenyAny(BasePermission):
            def has_permission(self, request, view):
            return False
            def has_object_permission(self, request, view, obj):
            return False

 

Similarly, the users can update the REST_FRAMEWORK code in the settings.py file by using the following code:

REST_FRAMEWORK = {
            'DEFAULT_PERMISSION_CLASSES': (
            'core.api.permissions.DenyAny',
            ),
            ...
}

Configuring djangorestframework-jwt

For launching the application, the public JWKS is downloaded for the verification of JWT. Following is a sample code used in settings.py for such purpose:

import json
from urllib import request
COGNITO_AWS_REGION = 'eu-central-1'
COGNITO_USER_POOL = 'eu-central-1_xxxxxx'
COGNITO_AUDIENCE = None
COGNITO_POOL_URL = None
rsa_keys = {}
if COGNITO_AWS_REGION and COGNITO_USER_POOL:
            COGNITO_POOL_URL = 'https://cognito-idp.{}.amazonaws.com/{}'.format(COGNITO_AWS_REGION, COGNITO_USER_POOL)
            pool_jwks_url = COGNITO_POOL_URL + '/.well-known/jwks.json'
            jwks = json.loads(request.urlopen(pool_jwks_url).read())
            rsa_keys = {key['kid']: json.dumps(key) for key in jwks['keys']}
JWT_AUTH = {
            'JWT_PAYLOAD_GET_USERNAME_HANDLER': 'core.api.jwt.get_username_from_payload_handler',
            'JWT_DECODE_HANDLER': 'core.api.jwt.cognito_jwt_decode_handler',
            'JWT_PUBLIC_KEY': rsa_keys,
            'JWT_ALGORITHM': 'RS256',
            'JWT_AUDIENCE': COGNITO_AUDIENCE,
            'JWT_ISSUER': COGNITO_POOL_URL,
            'JWT_AUTH_HEADER_PREFIX': 'Bearer',
}

Now, to decode the information received by accessing tokens, the customized mapping logic is used in the function named cognito_jwt_decode_handler having the following code in core/utils/jwt.py file:

import jwt
from jwt import DecodeError
from jwt.algorithms import RSAAlgorithm
from rest_framework_jwt.settings import api_settings
from django.contrib.auth import authenticate
def get_username_from_payload_handler(payload):
            username = payload.get('sub')
            authenticate(remote_user=username)
            return username
def cognito_jwt_decode_handler(token):
            options = {'verify_exp': api_settings.JWT_VERIFY_EXPIRATION}
            unverified_header = jwt.get_unverified_header(token)
            if 'kid' not in unverified_header:
            raise DecodeError('Incorrect authentication credentials.')
            kid = unverified_header['kid']
            try:
            public_key = RSAAlgorithm.from_jwk(api_settings.JWT_PUBLIC_KEY[kid])
            except KeyError:
            raise DecodeError('Can\'t find proper public key in jwks')
            else:
            return jwt.decode(
            token,
            public_key,
            api_settings.JWT_VERIFY,
            options=options,
            leeway=api_settings.JWT_LEEWAY,
            audience=api_settings.JWT_AUDIENCE,
            issuer=api_settings.JWT_ISSUER,
                  algorithms=[api_settings.JWT_ALGORITHM]
            )

Creating A Test View

For creating a test view some files need to be updated. For example, the serializers.py file will have the following piece of code to retrieve user information:

from rest_framework import serializers
from account.models import User
class UserSerializer(serializers.ModelSerializer):
            class Meta:
            model = User
            fields = '__all__'

Similarly, for the current user profile, the views.py file will have the following code implemented:

from rest_framework.generics import GenericAPIView
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated
from account.api.serializers import UserSerializer
class UserProfileAPIView(RetrieveModelMixin, GenericAPIView):
            serializer_class = UserSerializer
            permission_classes = (IsAuthenticated, )
            def get_object(self):
            return self.request.user
            def get(self, request, *args, **kwargs):
            return self.retrieve(request, *args, **kwargs)

Finally, the url.py file will contain the following information to form a path:

from account.api.views import UserProfileAPIView
urlpatterns = [
            path('admin/', admin.site.urls),
            path('api/v1/me', UserProfileAPIView.as_view(), name='my_profile'),
]

Running Server and Making A Request

The developers use the Postman platform for testing integration code. It sends a GET request to the API having Authorization with Bearer <access_token> in the headers. The token will be valid for 3600 seconds. This token will provide information regarding a specific user for verification. Following is the format of the GET request:

http://localhost:4000/api/v1/me

Therefore, implementing the method as mentioned above step by step helps to bootstrap an application in Django using the Amazon Cognito tool.