Bootstrapping Django App with Cognito: Personal Experience

Mar 26, 2024
/
18 min read
Bootstrapping Django App with Cognito: Personal Experience
Gleb Pushkov
Gleb Pushkov
Backend Tech Lead

Nowadays, more and more developers integrate their app with Single sign-on (SSO) services. It greatly increases the speed of Django development, because the basic routine is already implemented, tested, and hosted: sign-in, registration, reset password, 2FA, email and phone verification, welcome-email, and other. It’s especially useful if you have a microservice architecture – once a user is authorized, they can send requests to any of them.

Below, I’ll share tips on integration of such service from my experience, which includes working for Django Stars, a company that has been using Python and Django in demanding projects for over 13 years.

There are several solutions offered by the following services: Azure Active Directory B2C, AWS Cognito, Okta, Auth0, and others. They differ by functionality (ensure it meets your app needs, like multilanguage emails), and especially, by price.

AWS Cognito is the cheapest one (but be aware that using lambdas, 2FA, SNS could additionally generate associated costs which might not be originally mentioned). Also, it’s very flexible. You can customize workflow with Lambda Triggers. Namely, during authorization events, your custom AWS Lambda functions could be called wherever you need them to do whatever you want. Or you can create your own Authentication Challenges. If your application stack is hosted on AWS and managed via CloudFormation (or Terraform), it’s also handy to set up and configure Cognito as an additional resource of your IaC (but you should to be aware of some cautions mentioned after the Django integration part).

According to documentation, after successful authentication, Amazon Cognito API returns id_token, access_token and refresh_token. Both id_token and access_token are JSON Web Tokens and could be used to identify a user during API requests to the Django application. The id_token contains personal identity information such as name, email, and phone_number. The access_token doesn’t carry such information, so it’s more secure to use it as JWT encoded via base64, and everybody who gets this token can easily see personal data. Even if you have encrypted https connection, there are ways to steal the data, like SSL Stip (hint: configure HSTS headers!).

On a high-view level the authentication process will look like that:
Bootstrapping Django App with Cognito: Personal Experience 1

Cognito and Django: Bootstrapping an App

In this guide, I will cover a case of Django app development with Cognito when we want to have two types of users – back office users (to login and work with django-admin, session authorization) and application users (to interact with api endpoints; such users are registered in Cognito, jwt-authorization).

So let’s take a step-by-step look at the integration of Django and AWS cognito.

Step 1. Install packages

I found several Django libraries that help with Django/AWS Cognito integration, but it’s not hard to build flexible and easy-to-extend configuration on your own using DRF djangorestframework-jwt.

NOTE: The original library is no longer maintained, so we will use a fork of it (drf-jwt). Another alternative library for jwt is currently also looking for a maintainer.

pip install djangorestframework cryptography drf-jwt
By the way, speaking of an authentication backend for DRF, Django Cognito JWT might come in handy (the package is called django-cognito-jwt for the installation command).

Read Also: How to Develop APIs with Django REST Framework

Step 2. Create a User Pool in AWS Cognito

Sign-in into your AWS console and proceed to Cognito. Press Manage User Pools (the Identity pool is something different). Create a new user pool and configure attributes.

NOTE: once you set up required attributes, you wouldn’t be able to change them without re-creating a pool and losing all users’ passwords. By the way, you have to migrate your users on your own (more thoughts about that at the end of the article). I recommend having as few required attributes as possible, for example, only email.

Create two app clients. The one for a frontend app: `Enable username-password (non-SRP) flow for app-based authentication (USER_PASSWORD_AUTH)`.

And if you will need to access user pool data from the backend app, add one more client: `Enable sign-in API for server-based authentication (ADMIN_NO_SRP_AUTH)`.

For easy testing of integration you can enable “Hosted UI”. Sign-Up/Sign-In pages are provided by Cognito, so you can easily obtain JWT tokens and use them in Postman to ensure the configuration of the Django side was done in a proper way.
Bootstrapping Django App with Cognito: Personal Experience 2
To do this you need to specify Amazon Cognito domain in “Domain name” section, e.g.:
https://any-fancy-name-you-like.auth.eu-central-1.amazoncognito.com
In “App client settings” you need to enable any of “OAuth Flows” (let’s say Implicit grant) and at least “OAuth Scope” (openid). Also provide a callback URL – http://localhost:8000/admin.
After saving your changes, at the bottom of the same page you will see the “Launch Hosted UI” link. It will lead you to the login form. Create a user in the “Users and groups” tab and use its credentials to log in via Hosted UI. As a result, a browser will be redirected to callback URL which will have necessary tokens:
http://localhost:8000/admin#id_token=eyJraWQiOiJNdm…&access_token=eyJraWQiOiJqenIwdnRVK….&expires_in=3600&token_type=Bearer
Congrats, we obtained id_token and access_token!
To see what’s inside, go to https://jwt.io/ and put the token into debugger. For access_token payload would be:

Step 3. Create custom User model

It’s a good practice to override the default user model once you start your Django app development, otherwise, it will be painful to migrate on a mid-project phase.

NOTE: I would recommend setting up an abstract base model which would be used everywhere. It can be placed in the special app where general and non-related to business logic application code lives,  for example, core.

These are reasons why it’s helpful:

  1. uuid4 as a primary key – identifiers like 91eea07d-0742-4925-9c39-fb6d6e352f2a would be generated instead of regular incremental 12, 13, 14. Nobody will know how many Orders or Users do you have. Also, uuid4 is generated on Python-side, so you can know a PK before insertion. There are many discussions about the size, performance and other disadvantages, one of the good articles I found you may check here. In my experience, uuid4is really neat and it gives additional flexibility. For example, if you don’t know which table to lookup, you can query multiple of them until you get the desired row, since UUID would be unique across the whole database;
  2. created_at and updated_at – useful fields which will be added to all models;
  3. Informative __repr__ – extremely helpful if you’re using Sentry to collect errors and exceptions. In the stack trace you will see <User 75145554-4142-480b-bcfa-36840b315ba1> instead of <object at 0x1028ed080>
  4. Such a layer gives you the ability to override the behavior of all your models in the transparent way like we just did with __repr__, or with base fields.

Now let’s create a custom user at account/models.py:

And in settings.py change a default user model
AUTH_USER_MODEL = 'account.User'
Now we can run migrations.
After that, we move to the final step. Let’s register custom model in account/admin.py:

Step 4. Configure REMOTE_USER

In case when external authentication sources are used, additional configuration has to be done. The RemoteUserBackend (the so-called remote user in Django) creates a new User record in the database if it can’t find existing. This behavior can be changed by create_unknown_user, find more info in the docs.

Step 5. Configure DRF

settings.py

NOTE: To reduce the number of security issues that could happen due to inattentiveness, I would recommend to override the default permission class. In core/api/permissionsyou can put the following class:

settings.py

Step 6. Configure djangorestframework-jwt

On application launch, we need to download public JWKS that will be used to verify JWT.
settings.py

In a real project, settings have to be less hardcoded, something like that
COGNITO_AWS_REGION = env('COGNITO_AWS_REGION', default=None)
To master managing Django settings, I recommend checking this article by my colleague Alexander Ryabtsev. It’s really worth it.

Downloaded RSA keys look like that:

If you check a decoded id_token and access_token, their headers include
{
"kid": "Mvd6BSFCvQ+PbEOQCqOZd3CCSdd/d/mw+65R5uN1+r0=",
"alg": "RS256"
}

That specifies which JWKS should be used to validate a token. The “mapping” logic has to be implemented by our own in cognito_jwt_decode_handler:
core/utils/jwt.py

Usually, JWKS should be periodically rotated by auth service. And you may have a KeyError which indicates that you need to download new JWKS keys. It’s been more than 3 years already, but Cognito still hasn’t implemented this functionality.
A few notes about  get_username_from_payload_handler: sub in jwt payload carries unique UUID of authenticated user in Cognito User Pool. You can use it as pk for your users in a database, or, to be more independent, keep your own-generated UUID4 as pk for users, and store Cognito sub as username (it’s what we’re doing now). But be aware that this solution can add complexity to your implementation, if a client/frontend app works directly with Cognito and β€œknows” only sub, but not internal UUID4 of a user, which will be used in DB to build relations between models.

Step 7. Create test view

account/api/serializers.py

account/api/views.py

urls.py

Step 8. Run server and make a request

We will use a Postman to ensure integration code works as it should.
Create a new GET request to http://localhost:8000/api/v1/me and in Headers provide Authorization with value Bearer <access_token>. The token is valid for 1 hour, so you may need to obtain a new one via Hosted UI (Step 3).
Congratulations, everything is done!
(Full project sample may be found at my GitHub).

Debugging

Sometimes debugging is hard and it’s better to dig into the source code and check when and why any of the messages appear. For example {"detail": "Invalid signature."}actually means that there is no such user in the database that indicates that REMOTE_USER was not properly configured, as it has to create a new user, if it fails to find an existing one.

What’s Worth to Note when Working with AWS Cognito with Django

Below, I’ve listed a number of facts that I’ve faced when working with Cognito in Django. They aren’t either pros or cons. These are just peculiarities you should know when starting to develop a Django app authentication feature with Cognito and tips how to solve them.

  • Update: there is a new client configuration ‘Prevent User Existence Errors’ which solves the issue. If this configuration is not set (or you have an old Cognito pool), Sing-in and reset password API explicitly indicate that the user with provided email hasn’t registered: {__type: "UserNotFoundException", message: "User does not exist."} – by default anyone can test a list of emails and identify who’s already registered in your application. It’s worth to mention that Cognito has a built-in protection for that, but there are not much details how it works – e.g it works only for sign-in API, or reset password is protected too. To protect registration form, you may think about adding a captcha – seems like it’s possible, but there is no solution out of the box – it requires configuring custom auth lambda functions flow;
  • Things have changed while this article was being prepared: finally, “Amazon Cognito User Pools service now supports case insensitivity for user aliases”. It’s a new feature for new User Pools, but existing ones still have such a problem. I was very surprised when we found out that both usernames and emails were treated as case-sensitive. So β€œJohnSmith@example.com” is different from β€œjohnsmith@example.com,” that is different from β€œjOhNSmiTh@exmaple.com”. One option to fix that was the following: on the frontend side, the app made emails lowercase. Another idea was to fix that on the Cognito Pool level with Lambda function on the “PreSignUp” trigger, but it didn’t allow me to change the input values. Thus, the only way was to override email/username after a user was created in “PostConfirmation” lambda, which is not much elegant;
  • Depending on the type of application, you can face a problem when you need to display a list of users with name/picture/phone/other-field. In such a case, you need to retrieve users from Cognito each time when data is requested, or implement profiles’ sync logic and store a copy of all Cognito users in your database. It’s common for any SSO service, but it’s related to the next issue you may face;
  • No Lambda trigger on attribute update. If a user modifies one’s profile data via Cognito API, there is no callback which indicates that data has been changed. If you store a copy of Cognito data in your database (for convenience), you have to use some workarounds, like: fronted code has to notify your services explicitly when user data in Cognito has been successfully updated. And then you can pull the changes from User Pool;
  • Cognito allows the creation of multiple users with the same email until one of them becomes verified (might happen when the user double-clicks the submit button of the registration form). That’s OK, but you have to cover this case when working with User Pool via code/scripts;
  • No export users functionality. There is an npm package which can do that, or you need to implement this all by yourself;
  • You can’t export users’ password hashes (in the case if you want to change SSO provider or use your own auth services, or even if you want to move users from one User Pool to another). You will have to send an email asking them to set a new password with proper explanation. Afterwards, you’ll have to support a flow when users will start trying to login with their passwords;
  • During signup flow, users always have to enter login & password right after clicking on the confirmation link in the email. There is no way to auto-login the user. It gives a rise to multiple complaints, but it still hasn’t been fixed by Cognito;
  • No easy way to mark token as invalid when user changes password or signs out https://github.com/aws-amplify/amplify-js/issues/3435
  • And a few more things have been already covered here.

Cognito and CloudFormation: Things to Keep in Mind

CloudFormation is a great tool that allows you to store and maintain your infrastructure as a code. You can define multiple stacks, for each stack you need to create a special JSON template that defines your resources and configuration. Such files could be managed manually, or they could be generated by Troposphere via Python code (still keep in mind that when coding for growing infrastructure, it’s easy to fall into overengineering and end up with a hardly maintainable codebase. In this case managing flat YAML/JSON files would be a much better choice in long-term perspective). CloudFormation automatically rolls back all changes, if something goes wrong during a stack update what is a really great feature. But the way it manages Cognito is a bit strange, thus, I would like to highlight the most important notes.

  • If you created User Pool manually (as any other type of resources), it’s impossible to add it to CloudFormation stack without re-creation. CloudFormation can only manage resources that were created by CloudFormation. For “stateless” resources, like EC2, it’s not a problem, but for RDS it’s trickier as it will require a database restore from a snapshot not to lose data. However, with Cognito, it’s not possible to migrate easily, as it doesn’t allow to export passwords. Instead, this information will be lost and users will need to set up passwords again. Still, in such situation User Pool can be kept unmanaged, but you can use a reference (Ref) in the template to link it to the rest of your stack;
  • CloudFormation can drop the User Pool and all data you have inside. I found it out on my own experience in summer 2019 at our development environment (also mentioned in the referenced article above). Still, when I started the work on the article, half a year after the incident, and tried to reproduce the scenario (change the ordering of attributes in a template, or add a new one), it didn’t happen that time. Likely this was fixed, but I’m not sure for 100%. Actually, this was the main driver for me to share experience on working with this stack. Here are some advice you may to use just to be safe:

– ensure you provided DeletionPolicy. Even if something bad happens, the resource will be removed from a stack, but it wouldn’t be physically deleted (still will be accessible from a template using Ref);

– before applying a new template for each stack update, carefully review the change set. Look there for a User Pool resource name and for words like “changeSource”: “DirectModification” on this resource. That might be a mark that something might go wrong, depending on the changes. If AWS really fixed the issue, this point is not valid anymore, but personally I have a Vietnam Syndrome once I see that.

I’m not sure how Terraform or other tools deal with Cognito, but you have to be extreeeeeeemly careful here, otherwise you can accidentally kill the product.

Summary

Picture yourself as a barber and SSO service as a straight razor. It’s a great tool if you know how to deal with it, but otherwise the risk of making something wrong (with really bad consequences) is very high. It’s a tool that you have to pick wisely and configure carefully, put a lot of effort into investigating how it works, and even it β€˜s worth to spend time building a proof of concept to get a better feeling of its abilities and restrictions. Once you release an application and get first users, there is no way back: migration to another service could be very painful with import/export of user passwords being a slippy thing. Thus, I’m sure sharing of experience in this topic is very useful. If you have anything to say, highlight some pitfalls, or recommend something around SSO integration, please leave a comment, and one day it will help someone to build a successful story.

Also, if you are looking for consulting or a dedicated team to develop your software product, you are welcome to contact Django Stars.

Thank you for your message. We’ll contact you shortly.

Frequently Asked Questions
Is Amazon Cognito SaaS or PAAS?
Amazon Cognito is a service provided by Amazon Web Services (AWS) that falls into the category of Platform as a Service (PaaS). Cognito is a service that helps authenticate and authorize mobile and web applications and users. It provides features such as user registration, sign-in, and access control for application resources.
Are there alternatives to using Cognito in Django?

Yes, there are alternatives to using Amazon Cognito in Django, some of which are:

  1. Django's built-in authentication system: Django provides a built-in authentication system that handles user authentication, registration, and password management.
  2. Django Allauth: This is a third-party package for Django that provides additional authentication features such as social media authentication and email verification.
  3. Django Rest Framework (DRF) Authentication: DRF provides several authentication classes that can be used to authenticate Django Rest Framework views, such as TokenAuthentication and SessionAuthentication.
  4. Django OAuth Toolkit (DOT): This is a third-party package for Django that provides OAuth2 authentication for Django Rest Framework views.
  5. Other third-party packages, like Django-Auth-OIDC, python-social-auth, and django-rest-auth, that can be used for user authentication and management in Django.

Ultimately the choice of the library will depend on the project requirements and the team's expertise.

How to fix that Amazon Cognito in Django treats both usernames and emails as case-sensitive?
While the Amazon Cognito User Pools service supports case insensitivity for user aliases, it is a relatively new feature for new User Pools (but existing ones still have such a problem). Previously, one option to fix that was that on the frontend side, the app made emails lowercase. Another idea was to fix that on the Cognito Pool level with the Lambda function on the β€œPreSignUp” trigger, but it didn’t allow changing the input values. Thus, the only way was to override email/username after a user was created in β€œPostConfirmation” lambda, which is not much elegant.
How to integrate Django and AWS Cognito?
  1. Install packages
  2. Create a User Pool in AWS Cognito
  3. Create a custom user model
  4. Configure REMOTE_USER
  5. Configure DRF
  6. Configure djangorestframework-jwt
  7. Create a test view
  8. Run the server and make a request
Is there a way to auto-login users in Cognito?
Cognito does not provide a way to auto-login the user. During the signup flow, users always have to enter their login and password right after clicking on the confirmation link in the email.

Have an idea? Let's discuss!

Contact Us
Rate this article!
4 ratings, average: 5 out of 5
Very bad
Very good
Subscribe us

Latest articles right in
your inbox

Thanks for
subscribing.
We've sent a confirmation email to your inbox.

Subscribe to our newsletter

Thanks for joining us! πŸ’š

Your email address *
By clicking β€œSubscribe” I allow Django Stars process my data for marketing purposes, including sending emails. To learn more about how we use your data, read our Privacy Policy .
We’ll let you know, when we got something for you.