API security is cheaper when it is a design constraint. This note walks through auth, authorization, abuse controls, and auditability as product decisions.
A lightweight threat model
You do not need a 40-page threat model to ship. You need answers to four questions: who can call this, what can they do, what is expensive or dangerous, and what must be forensically visible.
Write those answers in the API design doc. If the answers change, the design doc is out of date and so is the security posture.
Authentication and authorization are different problems
Authentication proves who the caller is. Authorization decides what they may do. Collapsing them ("logged-in users can do anything") is how tenant data leaks.
Prefer short-lived tokens, explicit scopes or roles, and server-side checks on every privileged path — not just at the edge.
If authorization is only enforced in the UI, you do not have authorization — you have a suggestion.
- Map every endpoint to a permission, even if several share the same one.
- Default deny. New endpoints are closed until a policy opens them.
- Treat service-to-service credentials as higher privilege, not lower.
- Log both the identity and the authorization decision on sensitive actions.
Rate limits and cost amplification
Public and authenticated APIs both need abuse controls. Start with per-identity rate limits, separate limits for expensive endpoints, and hard caps on bulk operations.
Watch for amplification: an endpoint that triggers fan-out, email, or export can turn one request into a bill or an outage.
def check_rate_limit(identity, endpoint, config):
key = f"rl:{identity}:{endpoint}"
used = redis.incr(key)
if used == 1:
redis.expire(key, config.window_seconds)
if used > config.max_requests:
raise RateLimitExceeded(retry_after=config.window_seconds)
Audit trails and data minimization
For security-relevant actions, capture who, what, when, and from where — in a place application bugs cannot casually rewrite. Retention should match legal and operational needs, not disk capacity.
Return only the fields the client needs. Every extra field in a response is a future breach of a field you forgot was sensitive.
- Define an audit event schema early; retrofitting history is rarely possible.
- Redact tokens, secrets, and full PII from logs by default.
- Separate operational logs from security audit logs in retention and access.
Key takeaways
- Answer four threat-model questions in the design doc: who, what, what hurts, what is logged.
- Separate authentication from authorization and default-deny privileged paths.
- Rate-limit expensive endpoints and look for cost amplification.
- Treat audit events as a product schema, not an afterthought.
- Minimize response fields; sensitivity is often accidental surface area.