🔐 libre-fastapi-jwt
Production-ready JWT authentication for FastAPI — access/refresh tokens, cookie storage with CSRF protection, JWKS, WebSocket auth, and token revocation, all in one dependency.
Documentation: https://LibreNZ.github.io/libre-fastapi-jwt Source Code: https://github.com/LibreNZ/libre-fastapi-jwt
Actively maintained by Libre NZ. Originally forked from fastapi-jwt-auth, now extensively rewritten and kept current.
Features
| Feature | Details |
|---|---|
| Access & refresh tokens | Short-lived access tokens + long-lived refresh tokens |
| Fresh tokens | Require re-login for sensitive operations |
| Token revocation (denylist) | Plug in any backend — memory, Redis, database |
| Cookie storage | HttpOnly cookies with __Host- prefix by default |
| CSRF protection | Double-submit cookie pattern, enabled by default |
| Dual token locations | Use headers, cookies, or both simultaneously |
| Custom claims | Embed arbitrary data in any token |
| WebSocket auth | Authenticate long-lived WebSocket connections |
| JWKS support | Validate tokens issued by third-party providers |
| OpenAPI integration | Tokens appear in the Swagger UI "Authorize" button |
| Asymmetric signing | RS256, ES256, and other algorithms via cryptography |
Requirements
Python 3.12, 3.13, or 3.14.
Installation
pip install libre-fastapi-jwt
For asymmetric (RSA/EC) key signing:
pip install 'libre-fastapi-jwt[asymmetric]'
Dependencies
| Package | Version | Role |
|---|---|---|
| PyJWT | ^2.10.1 |
JWT encoding, decoding, and signature verification |
| FastAPI | >=0.115.8 |
Web framework integration (Depends, Request, Response, WebSocket) |
| cryptography | >=44.0.1 |
RSA/EC key operations for asymmetric signing algorithms |
| httpx | >=0.28.1 |
Async HTTP client used to fetch JWKS endpoints |
| pydantic-settings | ^2.7.1 |
Configuration loading and validation |
All five packages are well-established, actively maintained, and widely used in the Python ecosystem. cryptography is the only one with compiled C extensions; the rest are pure Python.
Auth flow
sequenceDiagram
participant C as Client
participant A as FastAPI App
C->>+A: POST /login (username + password)
A-->>-C: { access_token, refresh_token }
C->>+A: GET /protected (Authorization: Bearer <access_token>)
A-->>-C: 200 OK — protected data
Note over C,A: Access token expires (default 15 min)
C->>+A: POST /refresh (Authorization: Bearer <refresh_token>)
A-->>-C: { access_token } ← new token, no re-login needed
Quick start
Header-based auth
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from libre_fastapi_jwt import AuthJWT
from libre_fastapi_jwt.exceptions import AuthJWTException
from fastapi.responses import JSONResponse
app = FastAPI()
class Settings(BaseModel):
authjwt_secret_key: str = "change-me-in-production"
@AuthJWT.load_config
def get_config():
return Settings()
@app.exception_handler(AuthJWTException)
def authjwt_exception_handler(request, exc):
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
class LoginBody(BaseModel):
username: str
password: str
@app.post("/login")
def login(body: LoginBody, auth: AuthJWT = Depends()):
if body.username != "alice" or body.password != "secret":
raise HTTPException(status_code=401, detail="Bad credentials")
return {
"access_token": auth.create_access_token(subject=body.username),
"refresh_token": auth.create_refresh_token(subject=body.username),
}
@app.get("/protected")
def protected(auth: AuthJWT = Depends()):
auth.jwt_required()
return {"user": auth.get_jwt_subject()}
Refresh token flow
@app.post("/refresh")
def refresh(auth: AuthJWT = Depends()):
auth.jwt_refresh_token_required()
new_token = auth.create_access_token(subject=auth.get_jwt_subject())
return {"access_token": new_token}
Cookie auth with CSRF protection
Cookies are the recommended approach for browser-based apps. __Host- prefixed cookie names and CSRF protection are enabled by default.
from fastapi import Response
class CookieSettings(BaseModel):
authjwt_secret_key: str = "change-me-in-production"
authjwt_token_location: list = ["cookies"]
authjwt_cookie_secure: bool = True # HTTPS only
authjwt_cookie_csrf_protect: bool = True # on by default
@AuthJWT.load_config
def get_config():
return CookieSettings()
@app.post("/login")
def login(body: LoginBody, response: Response, auth: AuthJWT = Depends()):
if body.username != "alice" or body.password != "secret":
raise HTTPException(status_code=401, detail="Bad credentials")
# Sets HttpOnly access + refresh cookies and CSRF cookies in one call
auth.set_pair_cookies(
auth.create_access_token(subject=body.username),
auth.create_refresh_token(subject=body.username),
response,
)
return {"detail": "Logged in"}
@app.delete("/logout")
def logout(response: Response, auth: AuthJWT = Depends()):
auth.jwt_required()
auth.unset_jwt_cookies(response)
return {"detail": "Logged out"}
@app.get("/me")
def me(auth: AuthJWT = Depends()):
auth.jwt_required()
return {"user": auth.get_jwt_subject()}
Custom claims
@app.post("/login")
def login(body: LoginBody, auth: AuthJWT = Depends()):
...
token = auth.create_access_token(
subject=body.username,
user_claims={"role": "admin", "org": "acme"},
)
return {"access_token": token}
@app.get("/admin")
def admin_only(auth: AuthJWT = Depends()):
auth.jwt_required()
claims = auth.get_raw_jwt()
if claims.get("role") != "admin":
raise HTTPException(status_code=403, detail="Admins only")
return {"ok": True}
Configuration reference
| Key | Type | Default | Description |
|---|---|---|---|
authjwt_secret_key |
str |
— | Secret for HS256 signing (required for symmetric) |
authjwt_private_key |
str |
— | PEM private key for asymmetric signing |
authjwt_public_key |
str |
— | PEM public key for asymmetric verification |
authjwt_algorithm |
str |
"HS256" |
Signing algorithm |
authjwt_access_token_expires |
timedelta / int / False |
timedelta(minutes=15) |
Access token lifetime |
authjwt_refresh_token_expires |
timedelta / int / False |
timedelta(days=14) |
Refresh token lifetime |
authjwt_token_location |
list[str] |
["headers"] |
"headers", "cookies", or both |
authjwt_cookie_secure |
bool |
True |
Require HTTPS for cookies |
authjwt_cookie_samesite |
str |
"lax" |
"strict", "lax", or "none" |
authjwt_cookie_csrf_protect |
bool |
True |
Enable CSRF double-submit protection |
authjwt_denylist_enabled |
bool |
False |
Enable token revocation checks |
Full configuration options: https://LibreNZ.github.io/libre-fastapi-jwt
Async-native verification
jwt_required, jwt_optional, jwt_refresh_token_required, and fresh_jwt_required are all async methods. This matters for denylist/revocation checks — the only place in a JWT library where real I/O (Redis, database) can occur. Token creation and cookie management remain synchronous since they involve no I/O.
You must await these calls from your route handlers:
@app.get("/protected")
async def protected(Authorize: AuthJWT = Depends()):
await Authorize.jwt_required()
return {"hello": Authorize.get_jwt_subject()}
The denylist callback transparently supports both sync and async implementations:
# Sync callback (e.g. in-memory set)
@AuthJWT.token_in_denylist_loader
def check_denylist(decoded_token: dict) -> bool:
return decoded_token["jti"] in revoked_jtis
# Async callback (e.g. Redis)
@AuthJWT.token_in_denylist_loader
async def check_denylist(decoded_token: dict) -> bool:
return await redis.sismember("revoked_jtis", decoded_token["jti"])
Both forms work without any code changes on the caller side.
Security defaults
Out of the box, libre-fastapi-jwt applies secure defaults so you don't have to think about them:
__Host-cookie prefix — prevents cookie injection across subdomains and enforcesSecure+ root path- HttpOnly cookies — tokens are never accessible to JavaScript
- CSRF double-submit — on by default for cookie mode; protects
POST,PUT,PATCH,DELETE SameSite=Lax— sensible default; upgrade tostrictfor extra protection- Weak algorithm rejection — the library enforces algorithm hygiene to prevent
alg: noneattacks
Examples
The examples/ directory covers:
asymmetric.py— RSA/EC key signingdenylist_redis.py— token revocation with Rediswebsocket.py— WebSocket authenticationfreshness.py— fresh token requirements for sensitive actionsdual_token_location.py— headers + cookies simultaneouslymultiple_files/— multi-file project structure
Test coverage
The test suite doubles as a functional specification. Each file below is a self-contained runnable guide for a specific area of the library.
tests/test_config.py — Configuration
- Default values for all config options
- Non-expiring tokens (
Falsefor expires) - Missing secret key raises
RuntimeError - Denylist enabled without a callback raises an error
- Full round-trip loading of all options from an external settings source
tests/test_create_token.py — Token creation
- Parameter validation for access, refresh, and pair token creation
- Dynamic expiry overrides per token
- Audience, algorithm, and
user_claimstype checking - Custom claims are correctly embedded in the payload
tests/test_decode_token.py — Token decoding & claims
- Expiry, leeway, and decode error handling
- Extracting raw token, JTI, and subject from requests
- Issuer and audience validation (valid and invalid cases)
- Algorithm mismatch detection
- RS256 asymmetric signing — valid and invalid key scenarios
tests/test_headers.py — Header-based auth
- Missing JWT, missing Bearer prefix, malformed token
- Valid
Authorization: Bearer <token>flow - Custom claims embedded in JWT headers
- Extracting JWT headers from request context
- Custom header name and custom header type configuration
tests/test_cookies.py — Cookie-based auth & CSRF
- Warning when cookies not in token location
- CSRF cookies set/not-set based on configuration
- Unsetting all cookies (access, refresh, CSRF)
- Custom cookie key names
jwt_optionalwith CSRF variants- Full CSRF double-submit validation across multiple URL patterns
tests/test_token_types.py — Token type claims
- Custom token type claim names
- Custom access/refresh type values
- Tokens operating without type claims entirely
tests/test_token_multiple_locations.py — Dual token locations
- Subject retrieval from either headers or cookies
- Refresh via cookie
- Token setting and unsetting across both locations
tests/test_denylist.py — Token revocation
- Non-denylisted access and refresh tokens pass through
- Denylisted access and refresh tokens are rejected
tests/test_websocket.py — WebSocket authentication
- Missing token, wrong token type, valid access token
- Optional JWT over WebSocket
- Refresh-only and fresh-token-only endpoints
- Invalid WebSocket instance type
- Missing cookie, missing CSRF token, missing CSRF claim
- CSRF double-submit mismatch and valid CSRF over WebSocket
tests/test_url_protected.py — Protected endpoint enforcement
- Missing header → 401
- Refresh token rejected on access-only endpoint
jwt_required,jwt_optional,jwt_refresh_token_required,fresh_jwt_required
tests/test_kid.py — Key ID (kid) & JWKS
kidplaced in JOSE header (not body) for symmetric keyskidis the public key thumbprint for asymmetric keys- Mismatched
kidis rejected when validation is enabled - Matching
kidis accepted kidvalidation is opt-in (disabled by default)
License
MIT — see LICENSE.
Metadata
Release files for libre-fastapi-jwt 0.23.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| libre_fastapi_jwt-0.23.1.tar.gz | 23.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| libre_fastapi_jwt-0.23.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.0 kB
Release files / libre_fastapi_jwt-0.23.1.tar.gz
| Download URL | libre_fastapi_jwt-0.23.1.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f57a39a18bb2450a934e0b88d1bfcbd0c51204d84cd3770dc6e01a4b119cf1a8
|
|
BLAKE2b-256 checksum How to use checksums |
b75fd856bdff59d389fe603cb1da75175d2719a2186c6c11834353d1b80ca11e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Release files / libre_fastapi_jwt-0.23.1-py3-none-any.whl
| Download URL | libre_fastapi_jwt-0.23.1-py3-none-any.whl |
|---|---|
| Size | 21.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
719a85a704dbe98252a3317f39b9b6866f46e6307de8d047c9f6f163cea69279
|
|
BLAKE2b-256 checksum How to use checksums |
8b9e409cc5715834004d7cbb25a364ff39e978bb4e0d8fe049ee8092cd650549
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|