FastAPI Advanced: Security, Async and Production
Authentication and Tokens
In this lesson you will
- Hash and verify passwords safely
- Issue and validate a signed token
- Protect endpoints with a dependency and know the limits
Introduction
Most real APIs must know who is calling and what they may do. Authentication proves identity. Authorisation decides what that identity may do. This lesson builds a small, working login using hashed passwords and a signed JWT (JSON Web Token), and is honest about where it stops. Security is easy to get wrong, so for a real product use well-reviewed libraries, keep dependencies updated and consider a professional review or a hosted identity provider.
Install: pip install pyjwt "pwdlib[argon2]" python-multipart. The code was run with PyJWT 2.15, pwdlib 0.3 and FastAPI 0.162.
Never store plain passwords
Store only a slow, salted hash. If your database leaks, attackers should not get passwords. Use a purpose-built password hashing algorithm such as Argon2 (recommended by OWASP), never a fast hash like plain SHA-256, and never your own invention.
Python
from pwdlib import PasswordHash
password_hash = PasswordHash.recommended()
hashed = password_hash.hash("correct horse")
print(hashed[:10]) # $argon2id$
print(password_hash.verify("correct horse", hashed)) # TrueWhen I ran this the hash began with $argon2id$. Each hash includes its own random salt, so the same password produces different hashes.
Issuing a token
After a correct login, return a signed token. The client sends it with later requests.
Python
from datetime import datetime, timedelta, timezone
import jwt
SECRET_KEY = "load-this-from-the-environment" # never hard-code in real projects
def make_token(username: str) -> str:
exp = datetime.now(timezone.utc) + timedelta(minutes=15)
return jwt.encode({"sub": username, "exp": exp}, SECRET_KEY, algorithm="HS256")sub(subject) is who the token is for, andexpis when it expires.- The token is signed, not encrypted. Anyone holding it can read its contents, so never put secrets in it. The signature only proves it was not altered.
- Keep the lifetime short.
The login endpoint
Python
from typing import Annotated
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
@app.post("/token")
def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = USERS.get(form.username)
if not user or not password_hash.verify(form.password, user["hashed"]):
raise HTTPException(401, "Incorrect username or password")
return {"access_token": make_token(user["username"]), "token_type": "bearer"}OAuth2PasswordRequestForm reads username and password from form data, which needs the python-multipart package. Use the same message for both "no such user" and "wrong password", so attackers cannot tell which usernames exist.
Protecting endpoints
Python
from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
oauth2 = OAuth2PasswordBearer(tokenUrl="token")
def current_user(token: Annotated[str, Depends(oauth2)]) -> str:
error = HTTPException(401, "Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except InvalidTokenError:
raise error
user = payload.get("sub")
if user not in USERS:
raise error
return user
@app.get("/me")
def me(user: Annotated[str, Depends(current_user)]):
return {"user": user}Always pass algorithms=[...] explicitly when decoding. Results from running this:
GET /me (no token) 401 {'detail': 'Not authenticated'}
POST /token wrong password 401 {'detail': 'Incorrect username or password'}
POST /token correct 200, token_type bearer
GET /me with token 200 {'user': 'ada'}
GET /me with bad token 401 {'detail': 'Invalid credentials'}The /docs page gets an Authorize button that uses the same flow.
Quick quiz
What is true about a signed JWT?
What this does not do
- Rate limiting and lockouts against password guessing. Add them, for example at a gateway.
- Token revocation: a stolen token works until it expires. Short lifetimes, refresh tokens and a revocation list help.
- Secure key management: load
SECRET_KEYfrom the environment or a secrets manager, use a long random value and rotate it when needed. - HTTPS: without it, passwords and tokens can be intercepted. Always use HTTPS in production.
- Authorisation: being logged in is not the same as being allowed. Check ownership and roles per resource, for example that a user may only read their own records.
- Account recovery, email verification and multi-factor authentication.
- Where the client stores the token, which has its own risks in browsers.
Consider a hosted identity service or well-maintained auth library for production. Do not publish real secrets in a repository.
Try it yourself
Add a /admin endpoint that only a user with the role admin can call. Store the role in your user data, check it in a dependency and return 403 for other users. Write tests for 401 and 403.
Summary
- Store only slow, salted password hashes such as Argon2, and never plain text.
- Issue short-lived signed JWTs, validate them in a dependency and keep the signing key secret.
- Add HTTPS, rate limiting, per-resource authorisation and careful key management, since tokens alone are not complete security.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading