JWTManager
The last battery in the box is the JWTManager. It signs and verifies JSON Web Tokens, but with a small luxury on top: it rotates its signing secret on a schedule and keeps the recent secrets in the cache, so old tokens stay valid until they age out on their own.
Enabling JWT
This is the first battery that builds on two others: it needs both
EXT_SCHEDULING (to run the rotation) and EXT_CACHE (to store the
secrets). The manager will refuse to start otherwise:
[general]
SECRET_KEY = supersecret
[data]
REDIS_URI = redis://localhost:6379
[extensions]
EXT_SCHEDULING = 1
EXT_CACHE = 1
EXT_JWT = 1
On startup the manager mints its first secret and registers a job that rotates it every JWT_ROTARY_INTERVAL days. Each secret lives in the cache under its own key id, so a token signed with an older secret still verifies until that secret expires.
Encoding and decoding
The API is four methods: a sync and async pair for each direction. You hand
encode a payload and get a signed token back; decode verifies it
and returns the claims. The manager fills in the standard exp,
iat, nbf, iss and aud claims for you:
from webfluid.core.ext import jwt
async def issue_token(user_id: int) -> str:
return await jwt.aencode({"sub": str(user_id)})
async def read_token(token: str) -> dict:
# Raises if the token is invalid, expired or not yet valid.
return await jwt.adecode(token)
Plugged into a route, issuing a token on login and reading it back on a protected endpoint looks like this:
from fastapi import Request
from fastapi.exceptions import HTTPException
from fluid.services.auth import issue_token, read_token
async def login(request: Request):
data = await request.json()
# ... verify credentials here ...
token = await issue_token(user_id=1)
return {"token": token}
async def whoami(request: Request):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing token")
try:
claims = await read_token(auth.removeprefix("Bearer "))
except Exception:
raise HTTPException(status_code=401, detail="Invalid token")
return {"user_id": claims["sub"]}
Audiences and tuning
Both encode and decode take an audience argument
(default "default"). Audiences let one app mint tokens for different
consumers — an internal API versus a public one, say — and you map the short
names to their full audience strings in the config:
from webfluid.core.config import register_config
@register_config(10)
class Config:
SESSION_COOKIE_SECURE = True
JWT_ROTARY_INTERVAL = 15 # days between secret rotations
JWT_SECRET_LENGTH = 128 # bytes of randomness per secret
JWT_EXPIRY_DAYS = 30 # token lifetime
JWT_ALGORITHM = "HS256"
JWT_ISSUER = "MyApp"
JWT_AUDIENCES = {
"default": "Application",
"api": "PublicAPI"
}
That rounds out the battery set. With the database, migrations, mail, i18n, events, cache and tokens all in place, our app has grown into something real. Time to give it a face.
Continue reading
From here you can continue straight with the Frontend introduction.