Ryan Cherifa

On Securing Production APIs

On Securing Production APIs

February 25, 2026

security in production APIs is achieved through layered controls that constrain failure. Each layer addresses a different class of risk, and the objective is not theoretical completeness but predictable behavior under real conditions: traffic spikes, malformed input, and adversarial usage.


1. Authentication: Establishing Identity

JWT (User Authentication)

JWT is well-suited for stateless, distributed systems, but only under strict discipline.

The underlying pattern is stateless authentication: the server does not store session state; instead, identity is embedded in a signed token. This improves scalability but shifts responsibility to validation and lifecycle management.

Key constraints:

  • Short-lived access tokens (typically 5–15 minutes) reduce exposure if compromised
  • Refresh tokens extend sessions, but must be stored securely and rotated
  • Asymmetric signing (RS256) enables safe key rotation without invalidating all tokens
  • Claims (iss, aud, exp, ...) must be strictly validated
  • Tokens should not contain sensitive or mutable data

Revocation introduces state into a stateless model. Two practical approaches exist:

  • Token versioning (lightweight, scalable)
  • Blacklist/deny-list (more control, requires fast storage such as a Key-value database)

JWT is effective when treated as a bounded, verifiable assertion, not a general-purpose session container.


API Keys (Service & Client Access)

API keys follow a different pattern: caller identification, not user identity.

They are simple, but their strength comes from controlled scope and observability.

Key characteristics:

  • Stored hashed (same model as passwords)
  • Scoped permissions (read/write/admin)
  • Bound to rate limits and optionally IP ranges
  • Fully rotatable without downtime

Unlike JWT, API keys introduce long-lived credentials, so risk is managed through:

  • Tight scoping
  • Rate limiting
  • Monitoring and revocation

They are particularly effective for:

  • External integrations
  • Internal service-to-service calls (when full OAuth is unnecessary)

2. Authorization: Controlling Access

Authorization implements the pattern of policy enforcement over identity.

Even with correct authentication, systems fail when authorization logic is inconsistent or incomplete.

A practical model combines:

  • RBAC (Role-Based Access Control) for coarse-grained decisions
  • Resource-level checks for ownership and context

For example:

def can_access_resource(user, resource):
    return resource.owner_id == user.id or user.role == "admin"

The important detail is placement:

  • Endpoint-level checks allow early rejection and reduce load
  • Service-level checks ensure correctness regardless of entry point

This creates defense-in-depth within the application layer.


3. Rate Limiting & Abuse Control

Rate limiting enforces the pattern of bounded resource consumption. Without it, any public API is vulnerable to both malicious and accidental overload.

There are multiple layers where rate limiting can be implemented, each with tradeoffs.

Application-Level

  • Implemented directly in the backend (e.g., middleware)
  • Fine-grained control (user ID, endpoint cost, business logic)
  • Easy to integrate with authentication context

Limitations:

  • Consumes application resources before rejection
  • Harder to horizontally scale consistently across instances without shared storage

Distributed Layer (Key-value database)

This is the most useful production pattern.

  • Centralized counters (Key-value database) enable consistency across instances
  • Supports token bucket or sliding window algorithms
  • Low latency and high throughput

This layer enables:

  • Per-user limits
  • Per-API key limits
  • Cost-based limits (e.g., heavier endpoints consume more tokens)

Tradeoff:

  • Introduces external dependency (Key-value database must be highly available)

This is typically the core enforcement layer.

Below is a simple example using FastAPI and Redis illustrating the idea:

import time
import aioredis
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
redis = aioredis.from_url("redis://localhost:6379")

MAX_REQUESTS_PER_MIN = 100

async def rate_limit(request: Request):
    user_id = request.headers.get("X-User-ID")
    if not user_id:
        raise HTTPException(status_code=400, detail="Missing user ID")

    window = int(time.time() // 60)
    key = f"rate:{user_id}:{window}"

    # Increment counter atomically and get the new value
    count = await redis.incr(key)
    if count == 1:
        await redis.expire(key, 60)

    if count > MAX_REQUESTS_PER_MIN:
        raise HTTPException(status_code=429, detail="Rate limit exceeded")

@app.middleware("http")
async def middleware(request: Request, call_next):
    await rate_limit(request)
    response = await call_next(request)
    return response

@app.get("/protected")
async def protected():
    return {"status": "ok"}

Gateway / Edge Layer (Cloud / API Gateway)

Examples include managed gateways or edge proxies.

The pattern here is early rejection at the network boundary.

Advantages:

  • Blocks traffic before it reaches the application
  • Protects infrastructure cost
  • Handles IP-based limits efficiently

Limitations:

  • Limited context (no user identity unless forwarded)
  • Less flexible for business-specific logic

Best used for:

  • Global rate limits
  • IP throttling
  • Basic abuse protection

Recommended Architecture

In practice, effective systems combine layers:

  • Edge layer: coarse filtering (IP-based, global limits)
  • Key-value db backed layer: consistent distributed enforcement

This creates a hierarchy:

  • Cheap checks first
  • Expensive, precise checks later

4. CORS: Browser Constraint, Not a Security Boundary

CORS implements a client-side enforcement policy, not server-side security.

It restricts which browser origins are allowed to call the API, but does not prevent direct access from non-browser clients.

Correct configuration:

  • Explicit allowlist of trusted origins
  • No wildcard usage in production
  • Restriction of allowed methods and headers

CORS reduces unintended exposure in browser contexts but should never be relied upon for protection against abuse or unauthorized access.


5. Input Validation: Reject Invalid Data Early

Input validation follows the pattern of fail-fast boundary enforcement.

All external data must be assumed invalid until proven otherwise.

Key principles:

  • Strict schema validation (e.g., Pydantic)
  • Rejection of unknown fields (prevents silent misuse)
  • Explicit typing (no implicit coercion)
  • Length and size constraints on all inputs

Example:

class CreateUserRequest(BaseModel):
    email: EmailStr
    password: constr(min_length=12, max_length=128)

This layer eliminates:

  • Injection vectors
  • Data corruption
  • Unexpected edge cases in business logic

It is one of the highest leverage controls in API security.


6. Transport Security

Transport security enforces confidentiality and integrity in transit.

Requirements are straightforward:

  • HTTPS across all endpoints
  • TLS 1.2 or higher
  • HSTS headers to enforce secure connections

Without transport-level protection, all higher-level mechanisms can be bypassed via interception.


Conclusion: Defense-in-Depth Request Lifecycle

A properly secured request passes through multiple independent controls:

  1. TLS establishes secure transport
  2. Edge or gateway applies coarse rate limiting
  3. Authentication validates identity (JWT or API key)
  4. Distributed rate limiting enforces usage constraints
  5. Authorization verifies permissions
  6. Input validation enforces data integrity
  7. Business logic executes