API security is the most critical concern for modern web applications. In 2026, APIs handle authentication, user data, payments, and infrastructure — making them the primary attack surface. This guide covers the OWASP API Security Top 10, implementation patterns, and tools to protect your APIs in production.
📋 Table of Contents
OWASP API Security Top 10 (2023)
- Broken Object Level Authorization (BOLA)
- Broken Authentication
- Broken Object Property Level Authorization
- Unrestricted Resource Consumption
- Broken Function Level Authorization
- Unrestricted Access to Sensitive Business Flows
- Server-Side Request Forgery (SSRF)
- Security Misconfiguration
- Improper Inventory Management
- Unsafe Consumption of APIs
Authentication: JWT Best Practices
from datetime import datetime, timedelta, timezone
from typing import Optional
import jwt
import secrets
import hashlib
SECRET_KEY = secrets.token_hex(32) # Store in env var!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE = timedelta(minutes=15)
REFRESH_TOKEN_EXPIRE = timedelta(days=30)
def create_access_token(user_id: int, scopes: list[str] = []) -> str:
payload = {
"sub": str(user_id),
"type": "access",
"scopes": scopes,
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + ACCESS_TOKEN_EXPIRE,
"jti": secrets.token_hex(16), # JWT ID for revocation
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# Check if token is revoked (requires Redis/DB lookup)
if is_token_revoked(payload["jti"]):
raise jwt.InvalidTokenError("Token revoked")
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
# NEVER store secrets in JWTs
# ALWAYS verify signature
# Use short expiry + refresh tokens
# Store sensitive data server-side, not in JWT
Rate Limiting
from fastapi import FastAPI, Request, HTTPException
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/api/auth/login")
@limiter.limit("5/minute") # 5 login attempts per minute per IP
async def login(request: Request, credentials: LoginCredentials):
pass
@app.get("/api/users")
@limiter.limit("100/minute") # 100 requests per minute
async def list_users(request: Request, current_user: User = Depends(get_current_user)):
pass
# Also implement:
# - Per-user rate limits (not just per-IP)
# - Exponential backoff on repeated auth failures
# - Account lockout after N failed attempts
Input Validation
from pydantic import BaseModel, EmailStr, Field, validator, field_validator
import re
class CreateUserRequest(BaseModel):
name: str = Field(min_length=2, max_length=50, pattern=r"^[a-zA-Z\s'-]+$")
email: EmailStr
password: str = Field(min_length=8, max_length=128)
age: int = Field(ge=13, le=120)
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if not re.search(r"[A-Z]", v):
raise ValueError("Password needs uppercase letter")
if not re.search(r"[0-9]", v):
raise ValueError("Password needs a digit")
if not re.search(r"[!@#$%^&*]", v):
raise ValueError("Password needs special character")
return v
@field_validator("name")
@classmethod
def sanitize_name(cls, v: str) -> str:
# Strip dangerous characters even if pattern matches
return v.strip()
# NEVER trust client input
# Validate all fields: type, format, length, range
# Reject unexpected fields (use strict mode)
# Sanitize before storage and display
Object-Level Authorization (BOLA prevention)
# VULNERABLE: Trusts user-supplied ID
@app.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, current_user: User = Depends(get_current_user)):
return db.invoices.get(invoice_id) # ANY user can access ANY invoice!
# SECURE: Always verify ownership
@app.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, current_user: User = Depends(get_current_user)):
invoice = db.invoices.get(invoice_id)
if not invoice:
raise HTTPException(404, "Not found")
if invoice.user_id != current_user.id and current_user.role != "admin":
raise HTTPException(403, "Forbidden") # Don't reveal existence to unauthorized users
return invoice
# BEST PRACTICE: Filter by user_id in query
@app.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, current_user: User = Depends(get_current_user)):
invoice = db.invoices.get_for_user(invoice_id, user_id=current_user.id)
if not invoice:
raise HTTPException(404, "Not found")
return invoice
Security Headers
from fastapi import FastAPI
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
# HTTPS redirect (production only)
app.add_middleware(HTTPSRedirectMiddleware)
# CORS: be specific, never use "*"
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com", "https://www.myapp.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["Authorization", "Content-Type"],
)
# Security headers middleware
@app.middleware("http")
async def add_security_headers(request, call_next):
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"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = "default-src 'self'"
# Remove server header
response.headers.pop("server", None)
return response
Sensitive Data Protection
import bcrypt
import secrets
from cryptography.fernet import Fernet
# Password hashing (NEVER store plaintext)
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
# Encrypt sensitive data (credit cards, SSNs, etc.)
ENCRYPTION_KEY = Fernet.generate_key() # Store in secrets manager!
cipher = Fernet(ENCRYPTION_KEY)
def encrypt_pii(data: str) -> str:
return cipher.encrypt(data.encode()).decode()
def decrypt_pii(encrypted: str) -> str:
return cipher.decrypt(encrypted.encode()).decode()
# API key generation
def generate_api_key() -> str:
return "tp_" + secrets.token_urlsafe(32)
# Mask sensitive data in responses
def mask_credit_card(card: str) -> str:
return "*" * 12 + card[-4:]
def mask_email(email: str) -> str:
user, domain = email.split("@")
return user[:2] + "***@" + domain
API Security Checklist
- All endpoints require authentication (except public ones)
- Object-level authorization on every data access
- Rate limiting on auth, search, and expensive endpoints
- Input validation with strict schemas (Pydantic, Zod, Joi)
- HTTPS only, HSTS enabled
- Security headers (X-Frame-Options, CSP, CORS)
- Passwords hashed with bcrypt (cost factor 12+)
- Secrets in environment variables or secret manager
- SQL parameterized queries (no string concatenation)
- Audit logs for sensitive operations
- API versioning to allow deprecation
- Dependency scanning (pip-audit, npm audit)
API security in 2026 requires defense in depth. No single technique is sufficient — combine authentication, authorization, rate limiting, input validation, and monitoring. Run automated security scans (SAST, DAST) in your CI pipeline and review OWASP Top 10 quarterly. The cost of a breach far exceeds the cost of proper security implementation.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment