A DRF Authentication module for verifying JWT Token issued by AWS Cognito.
I wasn't able to find a DRF Authentication Module which simply allows you to
- Verify a JWT
- Attach a
user
object to request if verification is successful- Assumption:
user
object only requiresusername
attribute to be instantiated. Other attributes can be later added usingattach_attributes
hook provided.
- Assumption:
Every solution I came across does a lookup on User
model in database, thus defeating the statelessness of JWT.
I have engineered the frontend to send relevant tokens using cookies
. I just wanted to
- Verify the
JWT
stored in${ACCESS_TOKEN_KEY}
cookie. - Attach a
user
object to myrequest
if verification succeeds. - Avoid any DB lookups.
- Attach permissions to user depending on some of the JWT fields(
cognito:groups
in my case).
Created a DRF Authentication Module which simply looks at the cookies and verifies the user. If user verification succeeds,
a user
object is created and attached to request
object. Rest of the code can use request.user
to access the created user.
The authentication module picks up the JWT stored in cookie named ${ACCESS_TOKEN_KEY}
, verifies it, and creates a user if the verification succeeds. It only uses username
to instantiate the user
object.
Note: It is expected that username
is present in your JWT
as a claim. If you're using AWS Cognito, then your JWT will contain a username
attribute.
The following assignment operation happens
from django.contrib.auth.models import User
user = User(username=username) # Username is created using username claim present in your JWT
Then, the created user
object and the request
is passed to attach_attribute
hook, which attaches necessary permissions (or attributes) to your user object. The user
object is modified in-place. Finally, if everything succeeds, the user
object is returned by the authenticate
method, which in-turn attaches it to request
object. Now, you can use request.user
object anywhere in your downstream application.
Note that this module is designed to avoid DB lookups at all.
Kindly ensure that following environment variables are set:
ACCESS_TOKEN_KEY= # Points to the cookie key containing access token issued by AWS Cognito
AWS_COGNITO_APP_CLIENT_ID= # Your Cognito App client ID
AWS_COGNITO_REGION= # Region in which your Cognito Server exists
AWS_COGNITO_USER_POOL_ID= # Your Cognito User pool ID
Ensure that environment variables described in common sections are set
Add djognito.authentication.BaseCognitoAuthentication
to DEFAULT_AUTHENTICATION_CLASSES
in settings.py
. Finally, your REST_FRAMEWORK
dict should look like
REST_FRAMEWORK = {
....
'DEFAULT_AUTHENTICATION_CLASSES': (
"djognito.authentication.BaseCognitoAuthentication",
....
),
....
}
Ensure that environment variables described in common sections are set
- Create a class inheriting
BaseCognitoAuthentication
and override theattach_attributes
method. - Add that class in your
DEFAULT_AUTHENTICATION_CLASSES
list.
The following example attaches an attribute called groups
to user object. The downstream code can use request.user.groups
anywhere to access the groups to which user belongs.
This example assumes that every user is part of at least one AWS Cognito group.
from djognito.authentication import BaseCognitoAuthentication
from djognito.jwt_utils import verify_jwt
from rest_framework import authentication
from rest_framework import exceptions
import logging
logger = logging.getLogger(__name__)
class CognitoAuthentication(BaseCognitoAuthentication):
def attach_attributes(self, user, request):
access_token = request.COOKIES.get('accessToken', '')
claims = verify_jwt(access_token)
user.groups = claims['cognito:groups']
if len(claims['cognito:groups']) > 1:
logger.warning(
f'User {claims["username"]} belongs to multiple group: {claims["cognito: groups"]}.')
Assuming that the filename is authentication.py
and PYTHONPATH
is able to locate it, add authentication.CognitoAuthentication
to your DEFAULT_AUTHENTICATION_CLASSES
. Finally, your REST_FRAMEWORK
dict should look like
REST_FRAMEWORK = {
....
'DEFAULT_AUTHENTICATION_CLASSES': (
"authentication.CognitoAuthentication",
....
),
....
}
You can read more about the flow of control here
One of the primary usecase of JWT is stateless authentication. It allows you to assert the authenticity of a given user without performing a database lookup. There are two major advantages of this:
- Performance boost: database lookup may significantly increase your latency
- Separation of Resources: The whole application can be divided into two distinct set of resources:
AuthServer
andResourceServer
. This has following benefits:- One can have a separate team/workforce to ensure that
Auth
server meets standard compliances (HIPAA
,FedRAMP
etc). ResourcesServer
andAuthServer
can scale independently.
- One can have a separate team/workforce to ensure that
AWS Cognito can act as a AuthServer
for systems relying on Stateless Authentication. It uses JWT
standard for issuing tokens and takes care of user management (sign-up, sign-in, account verification, MFA, etc). You can read more about AWS Cognito here