Yet another fork of fast-jwt-auth
Project description
🔐 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file libre_fastapi_jwt-0.23.1.tar.gz.
File metadata
- Download URL: libre_fastapi_jwt-0.23.1.tar.gz
- Upload date:
- Size: 23.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f57a39a18bb2450a934e0b88d1bfcbd0c51204d84cd3770dc6e01a4b119cf1a8
|
|
| MD5 |
ea4901ded34aa308380526f033e3e8d8
|
|
| BLAKE2b-256 |
b75fd856bdff59d389fe603cb1da75175d2719a2186c6c11834353d1b80ca11e
|
File details
Details for the file libre_fastapi_jwt-0.23.1-py3-none-any.whl.
File metadata
- Download URL: libre_fastapi_jwt-0.23.1-py3-none-any.whl
- Upload date:
- Size: 21.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
719a85a704dbe98252a3317f39b9b6866f46e6307de8d047c9f6f163cea69279
|
|
| MD5 |
f9ba964c3c0124b4778cb74935bd8ae2
|
|
| BLAKE2b-256 |
8b9e409cc5715834004d7cbb25a364ff39e978bb4e0d8fe049ee8092cd650549
|