A modern, type-safe JWT authentication library for FastAPI with Pydantic integration - bringing harmony to your auth flow! πΆ
- π Type-safe JWT authentication with full Pydantic model support
- π FastAPI dependency injection - automatic JWT validation
- π Multiple token locations - headers, cookies, or both
- π‘οΈ CSRF protection for cookie-based authentication
- π WebSocket support with dedicated authentication methods
- π€ User claims as Pydantic models - strongly typed user data
- π« Token denylist/blacklist support for logout functionality
- π Asymmetric algorithms support (RS256, ES256, etc.)
- β 100% test coverage with comprehensive test suite
pip install fastapi-jwt-harmonyFor asymmetric algorithm support:
pip install fastapi-jwt-harmony[asymmetric]from fastapi import FastAPI, Depends
from pydantic import BaseModel
from fastapi_jwt_harmony import JWTHarmony, JWTHarmonyDep, JWTHarmonyBare
app = FastAPI()
# Define your user model
class User(BaseModel):
id: str
username: str
email: str
# Configure JWT (simple way with dict)
JWTHarmony.configure(
User,
{
"secret_key": "your-secret-key", # pragma: allowlist secret
"token_location": {"headers", "cookies"} # Support both
}
)
# Or use JWTHarmonyConfig for advanced configuration
# from fastapi_jwt_harmony import JWTHarmonyConfig
# JWTHarmony.configure(
# User,
# JWTHarmonyConfig(
# secret_key="your-secret-key", # pragma: allowlist secret
# token_location={"headers", "cookies"}
# )
# )
@app.post("/login")
def login(Authorize: JWTHarmony[User] = Depends(JWTHarmonyBare)):
# Authenticate user (your logic here)
user = User(id="123", username="john", email="john@example.com")
# Create tokens
access_token = Authorize.create_access_token(user_claims=user)
refresh_token = Authorize.create_refresh_token(user_claims=user)
# Set cookies (optional)
Authorize.set_access_cookies(access_token)
Authorize.set_refresh_cookies(refresh_token)
return {"access_token": access_token}
@app.get("/protected")
def protected_route(Authorize: JWTHarmony[User] = Depends(JWTHarmonyDep)):
# JWT automatically validated by JWTHarmonyDep
current_user = Authorize.user_claims # Typed User model!
return {"user": current_user, "message": f"Hello {current_user.username}!"}FastAPI JWT Harmony provides several dependency types for different authentication needs:
from fastapi_jwt_harmony import (
JWTHarmonyDep, # Requires valid access token
JWTHarmonyOptional, # Optional JWT validation
JWTHarmonyRefresh, # Requires valid refresh token
JWTHarmonyFresh, # Requires fresh access token
JWTHarmonyBare, # No automatic validation
)
@app.get("/public")
def public_endpoint(Authorize: JWTHarmony[User] = Depends(JWTHarmonyOptional)):
if Authorize.user_claims:
return {"message": f"Hello {Authorize.user_claims.username}!"}
return {"message": "Hello anonymous user!"}
@app.post("/sensitive-action")
def sensitive_action(Authorize: JWTHarmony[User] = Depends(JWTHarmonyFresh)):
# Requires fresh token (just logged in)
return {"message": "Sensitive action performed"}Enable secure cookie-based authentication with CSRF protection:
from fastapi import Response
JWTHarmony.configure(
User,
{
"secret_key": "your-secret-key", # pragma: allowlist secret
"token_location": {"cookies"},
"cookie_csrf_protect": True,
"cookie_secure": True, # HTTPS only
"cookie_samesite": "strict"
}
)
@app.post("/login")
def login(response: Response, Authorize: JWTHarmony[User] = Depends(JWTHarmonyBare)):
user = User(id="123", username="john", email="john@example.com")
access_token = Authorize.create_access_token(user_claims=user)
# Set secure cookies
Authorize.set_access_cookies(access_token, response)
return {"message": "Logged in successfully"}
@app.post("/logout")
def logout(response: Response, Authorize: JWTHarmony[User] = Depends(JWTHarmonyDep)):
Authorize.unset_jwt_cookies(response)
return {"message": "Logged out successfully"}Authenticate WebSocket connections with dedicated methods:
from fastapi import WebSocket, Query
from fastapi_jwt_harmony import JWTHarmonyWS, JWTHarmonyWebSocket
@app.websocket("/ws")
async def websocket_endpoint(
websocket: WebSocket,
token: str = Query(...),
Authorize: JWTHarmonyWS = Depends(JWTHarmonyWebSocket)
):
await websocket.accept()
try:
# Validate JWT token
Authorize.jwt_required(token)
user = Authorize.user_claims
await websocket.send_text(f"Hello {user.username}!")
except Exception as e:
await websocket.send_text(f"Authentication failed: {str(e)}")
await websocket.close()Implement secure logout with token blacklisting:
# In-memory denylist (use Redis in production)
denylist = set()
def check_if_token_revoked(jwt_payload: dict) -> bool:
jti = jwt_payload.get("jti")
return jti in denylist
# Configure with denylist callback
JWTHarmony.configure(
User,
{
"secret_key": "your-secret-key", # pragma: allowlist secret
"denylist_enabled": True,
"denylist_token_checks": {"access", "refresh"}
},
denylist_callback=check_if_token_revoked
)
@app.post("/logout")
def logout(Authorize: JWTHarmony[User] = Depends(JWTHarmonyDep)):
jti = Authorize.get_jti()
denylist.add(jti) # Add to denylist
return {"message": "Successfully logged out"}Comprehensive configuration with sensible defaults:
from datetime import timedelta
JWTHarmonyConfig(
# Core settings
secret_key="your-secret-key", # Required for HS256 # pragma: allowlist secret
algorithm="HS256", # JWT algorithm
token_location={"headers"}, # Where to look for tokens
# Token expiration
access_token_expires=timedelta(minutes=15),
refresh_token_expires=timedelta(days=30),
# Headers
header_name="Authorization",
header_type="Bearer",
# Cookies
cookie_secure=False, # Set True for HTTPS
cookie_csrf_protect=True, # CSRF protection
cookie_samesite="strict",
# Asymmetric keys (for RS256, ES256, etc.)
private_key=None, # For signing
public_key=None, # For verification
# Denylist
denylist_enabled=False,
denylist_token_checks={"access", "refresh"},
# Validation
decode_leeway=0, # Clock skew tolerance
decode_audience=None, # Expected audience
decode_issuer=None, # Expected issuer
)Support for RS256, ES256, and other asymmetric algorithms:
# Generate keys (example)
private_key = """-----BEGIN PRIVATE KEY----- # pragma: allowlist secret
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7...
-----END PRIVATE KEY-----""" # pragma: allowlist secret
public_key = """-----BEGIN PUBLIC KEY----- # pragma: allowlist secret
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7...
-----END PUBLIC KEY-----""" # pragma: allowlist secret
JWTHarmony.configure(
User,
JWTHarmonyConfig(
algorithm="RS256",
private_key=private_key, # For signing tokens # pragma: allowlist secret
public_key=public_key, # For verifying tokens # pragma: allowlist secret
)
)Run the comprehensive test suite:
# Install development dependencies
uv sync --dev
# Run tests
uv run pytest
# Run with coverage
uv run pytest --cov=fastapi_jwt_harmony# Clone the repository
git clone https://github.com/ivolnistov/fastapi-jwt-harmony.git
cd fastapi-jwt-harmony
# Install with development dependencies
uv sync --dev
# Run tests
uv run pytest
# Run linting
uv run ruff check src/
uv run mypy src/fastapi_jwt_harmony
uv run pylint src/fastapi_jwt_harmony- β 111 tests passing - Comprehensive test coverage
- β Type-safe - Full mypy compatibility
- β Modern Python - Supports Python 3.11+
- β Production ready - Used in production applications
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
- FastAPI for the amazing web framework
- Pydantic for data validation and settings management
- PyJWT for JWT implementation
- Original fastapi-jwt-auth for inspiration
Made with β€οΈ for the FastAPI community