How to add post-quantum tokens to FastAPI¶
A FastAPI service that issues bearer tokens signed with hybrid ML-DSA-65 + Ed25519 and rejects any token that fails verification. The file runs its own self-check with FastAPI's test client.
The problem¶
Most FastAPI authentication uses JWTs signed with HS256, RS256 or ES256. RS256 and ES256 rely on RSA and elliptic curves, which a quantum computer breaks. You want the same bearer-token flow with signatures that stay unforgeable, without changing how clients send the token.
Install¶
httpx is only needed for the self-check at the bottom of the file.
Complete working code¶
fastapi_pqc.py
"""FastAPI service that issues and checks hybrid ML-DSA-65 + Ed25519 signed tokens (pqblocks-jose)."""
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pqblocks_jose import Policy, decode_jws, encode_jws, generate_keypair
POLICY = Policy(
version=1,
kem="X25519+MLKEM768",
sig="MLDSA65+Ed25519",
kdf="HKDF-SHA256",
max_token_bytes=8192,
kdf_info="JOSE_PostQuantum_Traditional_Hybrid X25519MLKEM768",
sig_alg_std="ML-DSA-65-Ed25519",
)
# Demo only: a real service loads its signing key from a secret store at start-up.
SIGNING_KEYS = generate_keypair("ML-DSA-65-Ed25519")
app = FastAPI(title="Post-quantum signed tokens")
bearer = HTTPBearer()
@app.post("/token")
def issue_token(user: str) -> dict:
claims = {"sub": user, "aud": "api.example.com"}
signed = encode_jws(claims, POLICY, private_key=SIGNING_KEYS.private_key)
return {"access_token": signed.compact_serialization, "token_type": "Bearer"}
def verified_token(creds: HTTPAuthorizationCredentials = Depends(bearer)) -> str:
try:
result = decode_jws(token=creds.credentials, policy=POLICY, public_key=SIGNING_KEYS.public_key)
except Exception:
raise HTTPException(status_code=401, detail="invalid token")
if not result.signature_verified:
raise HTTPException(status_code=401, detail="invalid token")
return creds.credentials
@app.get("/me")
def me(token: str = Depends(verified_token)) -> dict:
return {"ok": True, "token_bytes": len(token)}
if __name__ == "__main__":
# Self-check without a server: issue a token, use it, then tamper with it.
from fastapi.testclient import TestClient
client = TestClient(app)
tok = client.post("/token", params={"user": "alice"}).json()["access_token"]
assert client.get("/me", headers={"Authorization": "Bearer " + tok}).status_code == 200
tampered = tok[:-8] + ("A" * 8 if not tok.endswith("A" * 8) else "B" * 8)
assert client.get("/me", headers={"Authorization": "Bearer " + tampered}).status_code == 401
print("issued, accepted, tampered token rejected: OK")
Run the self-check with python fastapi_pqc.py, or serve it with uvicorn fastapi_pqc:app. Output of the self-check:
How it works¶
sequenceDiagram
autonumber
participant C as Client
participant A as FastAPI service
C->>A: POST /token?user=alice
A->>A: encode_jws(claims, POLICY, ML-DSA-65 + Ed25519 private key)
A-->>C: access_token (compact JWS)
C->>A: GET /me Authorization: Bearer <token>
A->>A: decode_jws(token, POLICY, public key)
alt both signatures valid
A-->>C: 200 {"ok": true}
else any check fails
A-->>C: 401 invalid token
end
/tokensigns the claims with the service's composite private key.verified_tokenis a FastAPI dependency: it decodes the bearer token against the pinnedPOLICYand the public key, and turns every failure into401.- Any route that depends on
verified_tokenis protected; the handler only runs for a valid token.
Things to know¶
- Load keys, do not generate them per process. With several workers, each would otherwise have its own key and reject the others' tokens. Load the private key from a secret store at start-up and share the public key with verifiers.
- Token size. Hybrid ML-DSA-65 tokens are about 4.6 KB. Raise header limits on proxies (for example nginx
large_client_header_buffers) if needed. - Add the usual JWT checks. This recipe shows the signature layer. Add expiry (
exp), audience and issuer checks for your application.