Security Best Practices
Async applications handle many concurrent connections, making security both more critical and more complex. A single vulnerability can affect hundreds of simultaneous users. Here are the security patterns that matter most for async Python applications.
Input Validation with Pydantic
Never trust incoming data, especially in concurrent environments:
from pydantic import BaseModel, validator, EmailStr
import re
class UserInput(BaseModel):
username: str
email: EmailStr
age: int
@validator('username')
def validate_username(cls, v):
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', v):
raise ValueError('Username must be 3-20 characters, alphanumeric only')
return v
@validator('age')
def validate_age(cls, v):
if not 13 <= v <= 120:
raise ValueError('Age must be between 13 and 120')
return v
Pydantic validates data structure and types automatically. Custom validators add business logic validation. The regex pattern prevents injection attacks through usernames.
Async Input Validation
Add async validation for database checks:
from fastapi import FastAPI, HTTPException
app = FastAPI()
async def is_username_taken(username: str) -> bool:
"""Check if username exists in database"""
# Simulate async database check
await asyncio.sleep(0.1)
return username.lower() in ['admin', 'root', 'test']
Async validators can check against databases or external services without blocking the event loop.
Use async validation in endpoints:
@app.post("/users")
async def create_user(user_data: UserInput):
"""Create user with async validation"""
if await is_username_taken(user_data.username):
raise HTTPException(status_code=400, detail="Username taken")
# Process user creation
return {"message": "User created successfully"}
Password Hashing
Implement secure password hashing with bcrypt:
import bcrypt
import asyncio
class PasswordManager:
async def hash_password(self, password: str) -> str:
"""Hash password asynchronously"""
loop = asyncio.get_running_loop()
salt = bcrypt.gensalt()
def hash_sync():
return bcrypt.hashpw(password.encode('utf-8'), salt)
hashed = await loop.run_in_executor(None, hash_sync)
return hashed.decode('utf-8')
async def verify_password(self, password: str, hashed: str) -> bool:
"""Verify password asynchronously"""
loop = asyncio.get_running_loop()
def verify_sync():
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
return await loop.run_in_executor(None, verify_sync)
Using thread pools prevents the CPU-intensive hashing from blocking the event loop.
JWT Token Management
Create and verify JWT tokens securely:
import jwt
from datetime import datetime, timedelta
from fastapi.security import HTTPBearer
class TokenManager:
def __init__(self, secret_key: str):
self.secret_key = secret_key
self.algorithm = "HS256"
def create_token(self, user_id: str, permissions: list = None) -> str:
"""Create JWT access token"""
expire = datetime.utcnow() + timedelta(minutes=30)
payload = {
"sub": user_id,
"exp": expire,
"permissions": permissions or []
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
async def verify_token(self, token: str) -> dict:
"""Verify JWT token"""
try:
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
Keep token creation simple and focused on essential claims only.
Authentication Dependency
Create a reusable authentication dependency:
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer
security = HTTPBearer()
token_manager = TokenManager("your-secret-key")
async def get_current_user(credentials = Depends(security)):
"""Get authenticated user from token"""
token = credentials.credentials
payload = await token_manager.verify_token(token)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token")
return {"user_id": user_id, "permissions": payload.get("permissions", [])}
@app.get("/protected")
async def protected_endpoint(current_user = Depends(get_current_user)):
"""Protected endpoint example"""
return {"message": f"Hello user {current_user['user_id']}"}
This dependency can be reused across all protected endpoints.
Rate Limiting
Implement simple rate limiting:
import time
from collections import defaultdict, deque
class RateLimiter:
def __init__(self):
self.clients = defaultdict(lambda: deque())
async def is_allowed(self, client_id: str, max_requests: int = 100) -> bool:
"""Check if request is under rate limit"""
now = time.time()
client_requests = self.clients[client_id]
# Remove old requests (older than 60 seconds)
while client_requests and client_requests[0] < now - 60:
client_requests.popleft()
# Check limit
if len(client_requests) < max_requests:
client_requests.append(now)
return True
return False
rate_limiter = RateLimiter()
async def rate_limit_middleware(request, call_next):
"""Rate limiting middleware"""
client_ip = request.client.host
if not await rate_limiter.is_allowed(client_ip):
raise HTTPException(status_code=429, detail="Rate limit exceeded")
return await call_next(request)
This provides basic protection against abuse without complex dependencies.
Secure Database Queries
Always use parameterized queries:
import asyncpg
async def get_user_safely(user_id: str):
"""Safe database query with parameters"""
query = "SELECT id, username, email FROM users WHERE id = $1"
async with db_pool.acquire() as conn:
row = await conn.fetchrow(query, user_id)
return dict(row) if row else None
async def search_users_safely(search_term: str):
"""Safe search with input validation"""
if not search_term or len(search_term) > 100:
return []
query = """
SELECT id, username, email
FROM users
WHERE username ILIKE $1
LIMIT 50
"""
async with db_pool.acquire() as conn:
rows = await conn.fetch(query, f"%{search_term}%")
return [dict(row) for row in rows]
Never concatenate user input directly into SQL queries.
Security Headers
Add essential security headers:
async def security_headers_middleware(request, call_next):
"""Add security headers to responses"""
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
return response
app.middleware("http")(security_headers_middleware)
These headers provide basic protection against common web vulnerabilities.
Summary
Security in async applications requires:
Essential Practices
- Input Validation: Validate all user inputs with Pydantic
- Password Security: Use bcrypt with async thread pools
- Token Management: Implement secure JWT handling
- Rate Limiting: Protect against abuse and DDoS
- Database Security: Always use parameterized queries
- Security Headers: Add protective HTTP headers
Key Principles
- Never trust user input
- Use async-safe security libraries
- Implement defense in depth
- Monitor and log security events
- Keep dependencies updated
Each security measure builds upon the others to create a robust defense system for your async applications.