markomirosavljev / fastapi-cognito

Basic Cognito-Auth library for Python and FastAPI.
MIT License
43 stars 16 forks source link

FastAPI - Cognito

FastAPI library that ease usage of AWS Cognito Auth. This library provides basic functionalities for decoding, validation and parsing Cognito JWT tokens and for now it does not support sign up and sign in features.

Requirements

How to install

Pip Command

pip install fastapi-cognito

How to use

This is the simple example of how to use this package:

from fastapi import FastAPI

app = FastAPI()

All mandatory fields are added in CognitoSettings BaseSettings object. Settings can be added in different ways. You can provide all required settings in .yaml or .json files, or your global BaseSettings object. Note that userpools field is Dict and FIRST user pool in a dict will be set as default automatically if userpool_name is not provided in CognitoAuth object. All fields shown in example below, are also required in .json or .yaml file (with syntax matching those files.)

from pydantic_settings import BaseSettings
from pydantic.types import Any

class Settings(BaseSettings):
    check_expiration: bool = True
    jwt_header_prefix: str = "Bearer"
    jwt_header_name: str = "Authorization"
    userpools: dict[str, dict[str, Any]] = {
        "eu": {
            "region": "USERPOOL_REGION",
            "userpool_id": "USERPOOL_ID",
            "app_client_id": ["APP_CLIENT_ID_1", "APP_CLIENT_ID_2"] # Example with multiple ids
        },
        "us": {
            "region": "USERPOOL_REGION",
            "userpool_id": "USERPOOL_ID",
            "app_client_id": "APP_CLIENT_ID"
        },
        ...
    }

settings = Settings()

This example below shows how global BaseSettings object can be mapped to CognitoSettings and passed as param to CognitoAuth. If we were using .yaml or .json, we should call .from_yaml(path) or .from_json(path) methods on CognitoSettings object.

from fastapi_cognito import CognitoAuth, CognitoSettings

# default userpool(eu) will be used if there is no userpool_name param provided.
cognito_eu = CognitoAuth(
  settings=CognitoSettings.from_global_settings(settings)
)
cognito_us = CognitoAuth(
  settings=CognitoSettings.from_global_settings(settings), userpool_name="us"
)
from fastapi_cognito import CognitoToken
from fastapi import Depends

@app.get("/")
def hello_world(auth: CognitoToken = Depends(cognito_eu.auth_required)):
    return {"message": "Hello world"}

Optional authentication

If authentication should be optional, we can use cognito_eu.auth_optional

Example:

from fastapi_cognito import CognitoToken
from fastapi import Depends

@app.get("/")
def hello_world(auth: CognitoToken = Depends(cognito_eu.auth_optional)):
    return {"message": "Hello world"}

Custom Token Model

This feature adds possiblity to use any token type for authentication(e.g. parsing ID token).

In case your token payload contains additional values, you can provide custom token model instead of CognitoToken. If there is no custom token model provided, CognitoToken will be set as a default model. Custom model should be provided to CognitoAuth object, and should be set as type of auth variable for endpoint dependency.

Example:

class CustomTokenModel(CognitoToken):
    custom_value: Optional[str] = None

cognito = CognitoAuth(
    settings=CognitoSettings.from_global_settings(settings),
    # Here we provide custom token model
    custom_model=CustomTokenModel
)

@app.get("/")
# Type of `auth` should be custom token Class
def hello_world(auth: CustomTokenModel = Depends(cognito.auth_required)):
    return {"message": f"Hello {auth.custom_value}"}

Custom Cognito attributes

Custom attributes in Cognito starts with custom:, which is the issue for parsing this variable with pydantic because of the colon. To parse custom attributes, add the full name of Cognito attribute to Pydantic Field alias.

class CustomTokenModel(CognitoToken):
    custom_value: Optional[str] = Field(alias="custom:custom_attr")

Pydantic will automatically parse value by alias if specified. Make sure that you have default value set if attribute is optional.

OpenAPI docs authentication

To use tokens to authenticate requests using OpenAPI docs, you can create wrapper class.

from fastapi.security import HTTPBearer
from starlette.requests import Request
from fastapi_cognito import CognitoToken

class CognitoAuth(HTTPBearer):
    async def __call__(self, request: Request) -> CognitoToken:
        return await cognito.auth_required(request=request)

cognito_auth = CognitoAuth()

@router.get("/")
async def test_endpoint(auth: CognitoToken = Depends(cognito_auth)):
    return JSONResponse(
        status_code=200, content={"detail": "Success"}
    )

This will show button for adding authentication token to the request.