🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

API Security Best Practices 2026: OWASP Top 10, JWT and Rate Limiting

⏱️6 min read  ·  1,200 words

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.

OWASP API Security Top 10 (2023)

  1. Broken Object Level Authorization (BOLA)
  2. Broken Authentication
  3. Broken Object Property Level Authorization
  4. Unrestricted Resource Consumption
  5. Broken Function Level Authorization
  6. Unrestricted Access to Sensitive Business Flows
  7. Server-Side Request Forgery (SSRF)
  8. Security Misconfiguration
  9. Improper Inventory Management
  10. 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.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা