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.
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.
Debugging the Code
The debugging of such a complex system is often hard to perform. Therefore, the commonly used approach is to observe the source code to determine when and how the error message appears. For example, the error {“detail”: “Invalid signature.”} corresponds to a situation that there is no such user in the database, which indicates that the REMOTE_USER configuration is not correct. As the situation unfolds, there is a need to create a new user if the system can not find an existing user.
Things to Consider While Working with AWS Cognito
AWS Cognito provides feasible solutions to have easy access to AWS resources from users’ applications. Following are some of the notable features to consider while working with AWS Cognito. These distinctive characteristics are helpful when working to develop a Django app authentication feature with Cognito. There are also tips to work with these characteristics and solve possible problems.
- “Prevent User Existence Error” is a new client configuration that solves the update issue. If the user has not set this configuration or has an old Cognito pool, there is a need to sign in and reset the password API explicitly indicating that the user with the given email has not registered yet. Following is the message {__type: “UserNotFoundException,” message: “User does not exist.”}. Moreover, Cognito possesses built-in protection for identifying already registered emails in the application. Users can also add captcha for protection against registration forms.
- AWS Cognito User Pools service provides support to case insensitivity for user aliases. In older versions of the user Pools, there is still the problem of considering usernames and emails case sensitive. One option to fix the problem was that the application made emails lowercase on the frontend side. Another option was to fix the problem on the Cognito Pool level using the Lambda function on the PreSignUp trigger. However, the only feasible solution was to override email and username after creating a user in the PostConfirmation Lambda function.
- There can also be problems displaying a list of users with their names, pictures, phone numbers, and other fields. This problem often depends on the type of application. If such a problem arises, there is a need to retrieve users from Cognito at the data request. Another way is to implement profile sync logic and store a copy of all Cognito users in the database. It is usual for any Single Sign-On (SSO) service, but it could relate to further issues the user may face in the application.