JWTdown for FastAPI
Contents
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#
Install the package.
pip install jwtdown-fastapi
Create a file to hold your specialized
Authenticatorthat uses whatever way your application gets account information from a username/email.In the following code snippet, the
AccountRepoclass 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 theusername.In the following code snippet,
AccountandAccountOutare 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
emailproperty 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#
In your Python module that declares your FastAPI object, import JWTdown for FastAPI, import your
Authenticatorobject, 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 /tokento log in using form data, which sets a cookie and returns a JSON token for you to use in your API callsDELETE /tokento 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.
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.