JWTdown for FastAPI#

We wrote jwtdown-fastapi to help you reduce the boilerplate authentication code written for FastAPI. It’s based on the code that you can find at OAuth2 with Password (and hashing), Bearer with JWT tokens.

Quick start#

You can use this package to easily create the endpoints for the login and logout functionality using a hybrid cookie and JSON version of token distribution to ease your use on the front-end.

The easiest way to use JWTdown for FastAPI is to create an instance of the Authenticator.

Installation and customization#

  1. Install the package.

    pip install jwtdown-fastapi
    
  2. Create a file to hold your specialized Authenticator that uses whatever way your application gets account information from a username/email.

    In the following code snippet, the AccountRepo class is an object that implements the repository pattern. It can be anything, really, as long as it returns a dictionary or a Pydantic model when called with the username.

    In the following code snippet, Account and AccountOut are Pydantic models used in your application.

    # authenticator.py
    import os
    from fastapi import Depends
    from jwtdown_fastapi.authentication import Authenticator
    from queries.accounts import AccountRepo, AccountOut, Account
    
    
    class MyAuthenticator(Authenticator):
        async def get_account_data(
            self,
            username: str,
            accounts: AccountRepo,
        ):
            # Use your repo to get the account based on the
            # username (which could be an email)
            return accounts.get(username)
    
        def get_account_getter(
            self,
            accounts: AccountRepo = Depends(),
        ):
            # Return the accounts. That's it.
            return accounts
    
        def get_hashed_password(self, account: Account):
            # Return the encrypted password value from your
            # account object
            return account.hashed_password
    
        def get_account_data_for_cookie(self, account: Account):
            # Return the username and the data for the cookie.
            # You must return TWO values from this method.
            return account.username, AccountOut(**account.dict())
    
    
    authenticator = MyAuthenticator(os.environ["SIGNING_KEY"])
    

    You MUST implement the first three methods. If you do not have an email property on your account model, you must implement the last method, too, to return the unique identifier for the account.

Signing Key#

The SIGNING_KEY mentioned above should ideally be 20-40 characters long. jwtdown-fastapi uses HS256 as its default algorithm. HS256 is based on symmetric encryption. This means that all of your microservices will need to use the same string value if they all way to use the same JWT.

Danger

Signing keys are private You MUST not share your signing key in your repository or anywhere someone can get it. Secret management is the hardest part of having secrets.

Easy log in and log out#

  1. In your Python module that declares your FastAPI object, import JWTdown for FastAPI, import your Authenticator object, and include its routers.

    # main.py
    from authenticator import authenticator
    from fastapi import APIRouter
    
    app = FastAPI()
    app.include_router(authenticator.router)
    

    Now, by default, your application has two routes:

    • POST /token to log in using form data, which sets a cookie and returns a JSON token for you to use in your API calls

    • DELETE /token to log out, which deletes the cookie set with logging in

Requiring a valid token#

In any of your endpoints that you want to secure to a logged-in user, use the authenticator.get_current_account_data method as an injected dependency.

# router.py
from authenticator import authenticator
from fastapi import APIRouter

router = APIRouter()


@router.post("/api/things")
async def create_thing(
    account_data: dict = Depends(authenticator.get_current_account_data),
):
    pass

The get_current_account_data will look for a bearer token in the Authorization header, first. If it doesn’t find one there, then it will look for a cookie named “fastapi_token” (or whatever you configured it to be). When it finds one of those, it validates the value as a token.

Getting the current account data, if they exist#

In any of your endpoints that you want the current account data, but it’s optional, use authenticator.try_get_current_account_data.

# router.py
from typing import Optional
from authenticator import authenticator
from fastapi import APIRouter

router = APIRouter()


@router.get("/api/things")
async def get_things(
    account_data: Optional[dict] = Depends(authenticator.try_get_current_account_data),
):
    if account_data:
        return personalized_list
    return general_list

The get_current_account_data will look for a bearer token in the Authorization header, first. If it doesn’t find one there, then it will look for a cookie named “fastapi_token” (or whatever you configured it to be). When it finds one of those, it validates the value as a token.

Getting tokens from HTTP-only cookies#

Let’s say that you want to use a fetch that includes your browser’s cookies. That way, you can use the server to decode the data that you send it.

To do that, use the try_get_current_account_data and authenticator.cookie_name to send back a payload that contains the JWT for use in fetch calls to non-authenticating services, as well as the decoded account data from the JWT stored in the data that was returned from the get_account_data_for_cookie call when the person logged in.

Warning

Only for fetch with included credentials This will only work if you set credentials: 'include' as part of the options for your fetch or RTK Query query.

# router.py
from jwtdown_fastapi.authentication import Token
from .auth import authenticator


class AccountToken(Token):
    account: AccountOut


@router.get("/token", response_model=AccountToken | None)
async def get_token(
    request: Request,
    account: Account = Depends(authenticator.try_get_current_account_data)
) -> AccountToken | None:
    if account and authenticator.cookie_name in request.cookies:
        return {
            "access_token": request.cookies[authenticator.cookie_name],
            "type": "Bearer",
            "account": account,
        }

Letting people sign up for new accounts#

If you allow people to sign up for an account, you’ll want to hash their passwords and use the login functionality of the Authenticator.

To make sure the passwords are compatible with the Authenticator, you should use the hash_password method.

Then, you should use the login method of the Authenticator. You’ll need to create a form Pydantic model to hold the form information that was submitted to your sign-up endpoint.

# router.py
from fastapi import (
    Depends,
    HTTPException,
    status,
    Response,
    APIRouter,
    Request,
)
from jwtdown_fastapi.authentication import Token
from .auth import authenticator

from pydantic import BaseModel

from queries.accounts import (
    AccountIn,
    AccountOut,
    AccountRepo,
    DuplicateAccountError,
)

class AccountForm(BaseModel):
    username: str
    password: str

class AccountToken(Token):
    account: AccountOut

class HttpError(BaseModel):
    detail: str

router = APIRouter()


@router.post("/api/accounts", response_model=AccountToken | HttpError)
async def create_account(
    info: AccountIn,
    request: Request,
    response: Response,
    repo: AccountRepo = Depends(),
):
    hashed_password = authenticator.hash_password(info.password)
    try:
        account = repo.create(info, hashed_password)
    except DuplicateAccountError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Cannot create an account with those credentials",
        )
    form = AccountForm(username=info.email, password=info.password)
    token = await authenticator.login(response, request, form, repo)
    return AccountToken(account=account, **token.dict())

In the above code example, the code hashes the plain text password. Then, the accounts.create method creates a new account in the data store using the account information and the hashed password. It does not save the plain password to the data store because that Would Be BadTM.

Then, the code creates an AccountForm object that has the username and password attributes. All of that is passed to the login method of the Authenticator, which returns a Token object. We take that Token object and turn it into our own AccountToken object.