Bootstrapping a Django App with Cognito: Django Stars Insights
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:
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.
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:
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,uuid4
is 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;created_at
andupdated_at
β useful fields which will be added to all models;- 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>
- 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/permissions
you 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.
Read Also: Building an AI proof of concept
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.
- 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:
- Django's built-in authentication system: Django provides a built-in authentication system that handles user authentication, registration, and password management.
- Django Allauth: This is a third-party package for Django that provides additional authentication features such as social media authentication and email verification.
- 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.
- Django OAuth Toolkit (DOT): This is a third-party package for Django that provides OAuth2 authentication for Django Rest Framework views.
- 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?
- Install packages
- Create a User Pool in AWS Cognito
- Create a custom user model
- Configure REMOTE_USER
- Configure DRF
- Configure djangorestframework-jwt
- Create a test view
- 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.