Metadata-Version: 2.1
Name: djognito
Version: 0.1.1
Summary: Auth module for using AWS Cognito with DRF
Home-page: https://github.com/Prakash2403/Djognito
License: MIT
Keywords: AWS Cognito,Cognito,DRF,Authentication,Authorization
Author: Prakash2403
Author-email: rishurai24@gmail.com
Requires-Python: >3.6
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Requires-Dist: Django (>=3)
Requires-Dist: djangorestframework (>=3.13.1,<4.0.0)
Requires-Dist: python-jose[cryptography] (>=3.3.0,<4.0.0)
Project-URL: Repository, https://github.com/Prakash2403/Djognito
Description-Content-Type: text/markdown

# Djognito

A DRF Authentication module for verifying JWT Token issued by AWS Cognito.

- [Djognito](#djognito)
  - [Problem Statement](#problem-statement)
  - [My usecase](#my-usecase)
  - [Solution̦̦](#solution̦̦)
    - [TL;DR version](#tldr-version)
  - [A detailed view](#a-detailed-view)
  - [How to use this module](#how-to-use-this-module)
    - [Common](#common)
    - [Case 1: You only want to authenticate using JWT](#case-1-you-only-want-to-authenticate-using-jwt)
    - [Case 2: You want to authenticate using JWT and attach attributes/permissions](#case-2-you-want-to-authenticate-using-jwt-and-attach-attributespermissions)
  - [Appendix](#appendix)
    - [Pre-requisites](#pre-requisites)
      - [JWT](#jwt)
      - [AWS Cognito](#aws-cognito)

## Problem Statement

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 requires `username` attribute to be instantiated. Other attributes can be later added using `attach_attributes` hook provided.

Every solution I came across does a lookup on `User` model in database, thus defeating the _statelessness_ of JWT.

## My usecase

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 my `request` if verification succeeds.
*  Avoid any DB lookups.
*  Attach permissions to user depending on some of the JWT fields(`cognito:groups` in my case).


## Solution̦̦

### TL;DR version

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. 

## A detailed view

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
```python
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. 

## How to use this module

### Common

Kindly ensure that following environment variables are set:

```python
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 
```

### Case 1: You only want to authenticate using JWT

Ensure that environment variables described in [common](#common) sections are set

Add  `djognito.authentication.BaseCognitoAuthentication` to `DEFAULT_AUTHENTICATION_CLASSES` in `settings.py`. Finally, your `REST_FRAMEWORK` dict should look like

```python
REST_FRAMEWORK = {
    ....
    'DEFAULT_AUTHENTICATION_CLASSES': (
        "djognito.authentication.BaseCognitoAuthentication",
        ....
    ),
    ....
}
```

### Case 2: You want to authenticate using JWT and attach attributes/permissions

Ensure that environment variables described in [common](#common) sections are set

1. Create a class inheriting `BaseCognitoAuthentication` and override the `attach_attributes` method.
2. 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.

```python
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
```python
REST_FRAMEWORK = {
    ....
    'DEFAULT_AUTHENTICATION_CLASSES': (
        "authentication.CognitoAuthentication",
        ....
    ),
    ....
}
```

You can read more about the flow of control [here](#a-detailed-view)

## Appendix

### Pre-requisites

#### JWT

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` and `ResourceServer`. This has following benefits: 
  * One can have a separate team/workforce to ensure that `Auth` server meets standard compliances (`HIPAA`, `FedRAMP` etc).
  * `ResourcesServer` and `AuthServer` can scale independently.

#### AWS Cognito

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](https://aws.amazon.com/cognito/)

