Cyber security · API Security

API Security Checklist: Auth, Rate Limits, and Abuse Cases

The missing checklist that prevents “oops, we got scraped.”

Reading time: ~8–12 min
Level: All levels
Updated:

“Oops, we got scraped” is rarely a single bug. It’s usually a chain: an endpoint that leaks too much data, a token that lasts too long, a missing authorization check (hello, IDOR), and no rate limits to slow an attacker down. This post is a practical API security checklist for the things that actually bite teams: auth, rate limiting, and common abuse cases like enumeration, credential stuffing, and automated scraping.


Quickstart

If you only have 30–60 minutes, do these steps in order. They’re the highest ROI moves to stop the most common API failures: broken authorization, unlimited data extraction, and “silent” abuse.

1) Lock down identity + tokens

  • Require TLS everywhere; reject plain HTTP at the edge
  • Use short-lived access tokens (minutes), and rotate refresh tokens
  • Validate issuer/audience/expiry; don’t accept “none” algorithms
  • Store secrets in a secret manager (not in code or logs)

2) Add authorization checks per resource

  • Never trust client-provided user_id, tenant_id, or role
  • Enforce “subject can access object” on every read/write
  • Default-deny; allow by explicit policy
  • Test for IDOR: “can I fetch someone else’s object by ID?”

3) Put rate limits where it matters

  • Apply global + per-endpoint limits (login, search, exports)
  • Limit by multiple keys: IP + account + API key + tenant
  • Return 429 with a predictable body; add Retry-After if you can
  • Cap pagination and result size (scraping loves “unbounded”)

4) Plan for abuse, not just bugs

  • Define top abuse cases: scraping, enumeration, brute-force, bot signups
  • Log the right signals: actor, resource, result, latency (not secrets)
  • Alert on spikes and “impossible travel” patterns
  • Add a kill switch: revoke keys / block actors quickly
A simple priority rule

If your API is public or partner-facing, prioritize in this order: authorization correctnessrate limits + capstoken hygieneabuse monitoring. A perfect OAuth setup won’t save you from a missing object-level permission check.

Overview

This is a checklist-style guide to shipping an API that’s harder to exploit and cheaper to operate. It focuses on three areas that cause most real incidents: authentication (who are you?), authorization (are you allowed?), and abuse controls (what happens when a bot tries 10M requests?).

What this post covers

Area What can go wrong What you’ll implement
Auth (identity) Stolen tokens, weak keys, long-lived credentials Token validation, rotation, scoped access, secure storage
Authorization (permissions) IDOR, role confusion, tenant breakout Resource-level checks, default-deny policies, safe patterns
Rate limits + quotas Scraping, brute force, expensive queries, outages Global + per-route limits, caps, backoff, predictable 429s
Abuse cases (real attackers) Enumeration, credential stuffing, bots, multi-accounting Signals + alerts, throttles, anomaly rules, revocation

The goal isn’t “perfect security” (doesn’t exist). The goal is a set of guardrails that make abuse expensive, mistakes contained, and incidents recoverable.

Threat model in one line

Assume the client is hostile, the network is observable, tokens will leak eventually, and bots will try your most valuable endpoints first (login, search, export, password reset).

Core concepts

A good API security checklist is mostly about correctness under pressure: when you add new endpoints fast, when traffic spikes, and when someone intentionally tries to break your assumptions. These concepts help you build guardrails that keep working at scale.

Authentication vs authorization (and why people mix them up)

Authentication (AuthN)

Proves identity: “Who is this request acting as?”

  • Sessions, access tokens, API keys, mTLS client certs
  • Token verification (issuer, audience, expiry, signature)
  • Key rotation and revocation

Authorization (AuthZ)

Grants permission: “Is this identity allowed to do this to this resource?”

  • Scopes/roles/claims are inputs, not the decision itself
  • Object-level permission checks (resource ownership / tenant)
  • Default-deny policies and explicit allow rules
The most expensive mistake

“We have JWTs, so we’re secure” is a trap. JWTs solve identity. They don’t automatically solve authorization. Most data leaks come from missing object-level checks, not from missing login.

Threat-driven design: protect what’s valuable and abusable

Attackers don’t scan every endpoint equally. They focus on endpoints that: (1) return lots of data, (2) change account state, or (3) cost you money. A practical way to prioritize is to label each route with data sensitivity and abuse risk.

Endpoint type Common abuse pattern Minimum controls
Login / OTP / password reset Credential stuffing, brute force, SMS pumping Strict rate limits, bot checks, lockouts, anomaly alerts
Search / list / analytics Scraping, enumeration, expensive queries Pagination caps, query constraints, per-tenant quotas
Exports / bulk downloads Data exfiltration, “one-click” scraping Explicit permissions, slow-path review, logging + alerts
Write actions (billing, role changes) Fraud, privilege escalation Strong auth, step-up checks, idempotency, audit trails

Rate limiting vs quotas (they solve different problems)

Rate limiting

Controls burst: requests per second/minute.

  • Stops brute force and request floods
  • Protects your infrastructure during spikes
  • Pairs well with “retry with backoff” clients

Quotas / budgets

Controls total usage: requests per day/month or cost units.

  • Stops slow scraping over weeks
  • Aligns with billing/tiers
  • Prevents “accidental” runaway clients

Abuse cases: think like a bot

Abuse is different from hacking: the attacker may use perfectly valid requests. Your job is to make misuse detectable and expensive without punishing legitimate users.

Common API abuse patterns

  • Scraping: walking IDs, harvesting search results, bulk export abuse
  • Enumeration: probing for existing users/objects via error messages
  • Credential stuffing: leaked password lists + automated login
  • Replay: resending captured requests (webhooks, signed requests)
  • Resource exhaustion: expensive filters, wide date ranges, huge payloads

Controls that work in practice

  • Uniform errors for “not found” vs “not allowed” where appropriate
  • Pagination + field selection + max limits (caps everywhere)
  • Per-route limits for risky endpoints + progressive throttling
  • Audit logs + anomaly alerts + fast revocation paths
  • Idempotency keys for writes; signature/timestamp for webhooks
The “caps” mindset

Most scraping incidents happen because APIs are too generous by default: unlimited page sizes, unlimited filters, unlimited search results, unlimited retries. Add caps early; relaxing caps later is easier than tightening them after an incident.

Step-by-step

This is a practical implementation path you can apply to an existing API or bake into a new one. The key is to avoid “security as a feature” and instead build default-safe patterns that every endpoint inherits.

Step 1 — Inventory endpoints and classify risk

Start by listing every route (including internal/admin) and adding two labels: data sensitivity (public, internal, PII, financial) and abuse risk (low, medium, high). This immediately tells you where to focus rate limits, scopes, and audit logging.

Mini checklist

  • Mark endpoints that return lists/search/export (scraping magnets)
  • Mark endpoints that change permissions/billing (high impact)
  • Mark “expensive” endpoints (complex joins, external calls, ML inference)
  • Decide: what’s the maximum data any single request can return?

Step 2 — Choose an auth strategy (and document it)

Your API security checklist should start with one question: Who calls this API? A browser app, a mobile app, server-to-server integrations, or partners? Different callers imply different credentials. Whatever you choose, document it in OpenAPI so it’s visible and reviewable.

Rule of thumb

Use OAuth/OIDC for user-based access and delegated permissions. Use API keys (or mTLS) for server-to-server integrations, but treat keys like passwords: rotate, scope, and revoke.

Here’s a compact OpenAPI example showing bearer tokens and per-endpoint security requirements. This doesn’t implement security by itself, but it forces clarity and makes reviews much easier.

openapi: 3.0.3
info:
  title: Example API
  version: 1.0.0

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

paths:
  /v1/users/{id}:
    get:
      summary: Get a user (self or admin)
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
        "401":
          description: Unauthorized
        "403":
          description: Forbidden

Step 3 — Implement authorization as a policy, not scattered if-statements

Authentication is a gate. Authorization is the map of what’s allowed. The safest pattern is: (1) resolve the actor, (2) resolve the resource, (3) ask a policy function if the action is allowed. That gives you a single place to test, audit, and change behavior.

What “good” looks like

  • One policy function per resource type (user, org, project)
  • Resource ownership checks (including tenant boundaries)
  • Explicit deny for unknown roles/scopes
  • Auditable decisions (why was access granted?)

What to avoid

  • “If admin then allow everything” without scoping
  • Client-supplied IDs used to fetch other users’ data
  • Relying on UI to hide buttons instead of API enforcement
  • Different authorization logic across endpoints for the same resource

Step 4 — Add request guards: validation, caps, and safe defaults

Most abuse relies on endpoints being too flexible. Add guardrails so requests stay inside safe bounds: validate input, cap pagination, and reject payloads that exceed your limits. This improves both security and reliability.

Guard Why it matters Default suggestion
Max page size Prevents bulk extraction per request 25–100 depending on object size
Max filter width Stops expensive “query everything” requests Whitelist fields; reject unknown filters
Max payload size Prevents memory pressure and parsing abuse Limit at edge + app layer
Uniform error shapes Reduces enumeration signal Consistent codes; avoid verbose hints

Step 5 — Implement rate limits that match abuse patterns

Rate limiting should be multi-dimensional (IP, account, tenant, API key) and route-aware. A single global limit is better than nothing, but it won’t stop targeted abuse (e.g., login or export endpoints).

Where to apply limits

  • Edge gateway/WAF (cheap, early)
  • App middleware (context-aware)
  • Data layer protections (query timeouts, statement limits)

Good limiter keys

  • IP: good for anonymous traffic (but NAT/shared IPs exist)
  • API key: best for integrations
  • User/account: best for logged-in actions
  • Tenant/org: prevents one customer taking you down

Below is a practical Node/Express example that: (1) validates a bearer token, (2) enforces a per-user rate limit, and (3) applies a stricter limit for a high-risk route (login).

import express from "express";
import rateLimit from "express-rate-limit";
import jwt from "jsonwebtoken";

const app = express();
app.use(express.json());

// 1) Global baseline: protects your infra from obvious floods
app.use(rateLimit({
  windowMs: 60_000,
  limit: 300,
  standardHeaders: true,
  legacyHeaders: false
}));

function requireAuth(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.status(401).json({ error: "unauthorized" });

  try {
    // In production: validate issuer/audience and use a JWKS-based verifier for OIDC providers.
    const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY, { algorithms: ["RS256"] });
    req.user = { id: payload.sub, role: payload.role, scope: payload.scope || "" };
    return next();
  } catch {
    return res.status(401).json({ error: "unauthorized" });
  }
}

// 2) Per-user limiter (fallback to IP if unauthenticated)
const userLimiter = rateLimit({
  windowMs: 60_000,
  limit: 120,
  keyGenerator: (req) => (req.user?.id ? `u:${req.user.id}` : `ip:${req.ip}`),
  standardHeaders: true,
  legacyHeaders: false
});

app.get("/v1/me", requireAuth, userLimiter, (req, res) => {
  res.json({ id: req.user.id });
});

// 3) High-risk endpoint: stricter limiter
app.post("/v1/login", rateLimit({ windowMs: 60_000, limit: 10 }), (req, res) => {
  res.status(501).json({ error: "not_implemented" });
});

app.listen(3000);
Gotcha: rate limits can become an oracle

If you only rate-limit “valid users,” you can accidentally leak whether an account exists. Prefer limits that apply before revealing existence (especially for login/reset flows), and keep errors consistent.

Step 6 — Harden against replay and webhook abuse

Webhooks and signed requests are common “forgotten” surfaces. The core idea is simple: include a timestamp and signature, verify both, and reject old requests. This prevents replay (an attacker re-sends a captured request later).

Example: verify a webhook signature using HMAC, enforce a maximum clock skew, and compare signatures safely. This belongs in your API security checklist if you accept inbound events from third parties.

import hmac
import hashlib
import time

MAX_SKEW_SECONDS = 300  # 5 minutes

def verify_webhook(secret: str, body: bytes, header_sig: str, header_ts: str) -> bool:
  """
  header_sig example: "v1=hex_digest"
  header_ts: unix timestamp as string
  """
  try:
    ts = int(header_ts)
  except (TypeError, ValueError):
    return False

  # Reject replays / very old requests
  if (time.time() - ts) > MAX_SKEW_SECONDS:
    return False

  # Sign (timestamp + "." + body) to bind time + payload
  msg = str(ts).encode("utf-8") + b"." + body
  digest = hmac.new(secret.encode("utf-8"), msg, hashlib.sha256).hexdigest()
  expected = f"v1={digest}"

  # Constant-time comparison to avoid timing attacks
  return hmac.compare_digest(expected, header_sig)

Step 7 — Add observability: logs you can use during an incident

Logging isn’t “nice to have” for security. It’s your reconstruction tool when something goes wrong. Log enough to answer: who did what to which resource, from where, and what happened.

Log these fields

  • request_id / trace_id
  • actor: user_id, api_key_id, tenant_id
  • route + method + status code + latency
  • resource id(s) accessed (when applicable)
  • abuse signals: rate-limit hit, auth failure reason (coarse)

Never log these

  • Passwords, one-time codes, raw tokens
  • Authorization headers or full cookies
  • Secret keys (webhook secrets, API private keys)
  • Full PII payloads (log minimal references instead)

Step 8 — Test the checklist (don’t trust “it seems fine”)

A checklist is only real when it’s tested. Add automated tests for the scariest failures: missing authorization, privilege escalation, and “infinite” list endpoints.

High-value tests

  • IDOR tests: user A cannot read/write user B’s resources
  • Scope tests: token without scope cannot call protected route
  • Rate limit tests: repeated requests eventually return 429
  • Pagination cap tests: page_size above max is rejected or clamped
  • Abuse flow tests: login/reset endpoints behave consistently

Common mistakes

These are patterns behind “we had auth, why did we still leak data?” The fixes are usually straightforward, but you have to be intentional because frameworks don’t automatically enforce them.

Mistake 1 — Confusing AuthN with AuthZ

Valid token ≠ permission. A token can be real and still not allowed to access a specific object.

  • Fix: enforce resource-level checks on every read/write.
  • Fix: test IDOR explicitly (the #1 API data leak pattern).

Mistake 2 — Trusting client-provided identifiers

If the client can pick user_id or tenant_id, an attacker will pick someone else’s.

  • Fix: derive identity from verified tokens (server-side), not request JSON.
  • Fix: ignore/override sensitive fields even if the UI hides them.

Mistake 3 — Unbounded lists and “free” exports

Scraping loves endpoints with large pages, rich fields, and unlimited filters.

  • Fix: cap pagination and limit fields; require explicit permissions for exports.
  • Fix: add quotas and route-specific throttles for list/search endpoints.

Mistake 4 — Rate limiting only by IP

Bots rotate IPs; legitimate users share IPs (NAT, mobile networks). IP-only is noisy and bypassable.

  • Fix: limit by API key / account / tenant, with IP as a fallback.
  • Fix: use stricter limits on login/reset and other high-risk routes.

Mistake 5 — Leaking signals through errors

“User not found” and “wrong password” are useful to attackers. Same for “object exists but forbidden”.

  • Fix: keep external errors consistent; log detailed reasons internally.
  • Fix: consider returning 404 for unauthorized access to private objects (context-dependent).

Mistake 6 — Logging secrets and tokens

This turns your logs into a credential vault. Leaked logs become leaked access.

  • Fix: redact Authorization headers, cookies, and sensitive fields.
  • Fix: add log-scanning rules to CI and runtime observability.
Fast self-audit

Pick your most sensitive list endpoint and ask: “How many requests would it take to export the entire dataset?” If the answer is “not many,” tighten caps, add quotas, and require explicit permission for bulk access.

FAQ

Do I need OAuth if I already use API keys?

Not always. API keys work well for server-to-server integrations, but they’re coarse-grained unless you add scopes/permissions on top. OAuth/OIDC shines when access is tied to a user identity, delegated access (third-party apps), or you need standard flows like authorization code. Many teams use both: OAuth for user access and API keys for partner integrations.

What’s the difference between a scope and authorization?

A scope is a hint; authorization is the decision. Scopes (or roles/claims) describe what a token might be allowed to do, but your API still needs to check the specific resource: user A with “read” scope should not automatically read user B’s private object.

How do I choose rate limit numbers?

Start with intent and risk. Set looser limits for low-risk reads and stricter limits for high-risk routes (login/reset, exports, search). Use observed production traffic to tune. The important part is having some limit, returning predictable 429s, and limiting by the right keys (account/tenant/API key), not only IP.

How do I prevent scraping if the API is public?

You can’t “ban scraping” with one trick. You reduce it by adding caps (page size, fields), introducing quotas, requiring authentication for higher-value data, using route-specific throttles, and watching for automated behavior (high fan-out, sequential ID access, abnormal user agents). Also consider “bulk access” as a separate product feature with explicit permissions and audit logs.

Should I return 404 or 403 for unauthorized resources?

It depends on your enumeration risk. Returning 404 for unauthorized access can hide whether a resource exists, which reduces enumeration. Returning 403 is clearer for legitimate users. A common compromise: use 404 for sensitive resources and keep detailed reasons in internal logs.

How do I secure webhooks and signed requests?

Verify signatures and prevent replay. Use an HMAC (or asymmetric signature), include a timestamp, reject old requests, and use constant-time signature comparison. Also log verification failures and rate-limit webhook endpoints to prevent floods.

Cheatsheet

A scan-fast API security checklist you can paste into a PR description, run during a launch review, or keep as your “definition of done”.

Auth (identity)

  • TLS everywhere; HSTS where applicable
  • Tokens validated (signature, issuer, audience, expiry)
  • Short-lived access tokens; refresh tokens rotated
  • Keys/tokens stored securely; no secrets in logs
  • Revocation path exists (disable user / revoke key)

Authorization (permissions)

  • Object-level checks on every read/write (IDOR-proof)
  • Tenant boundaries enforced (org_id is server-derived)
  • Default-deny policies; explicit allow rules
  • “Admin” behavior scoped and audited
  • Tests cover: self vs other, role boundaries, scope boundaries

Rate limits + caps

  • Global limit at edge + route-specific limits for risky endpoints
  • Limits keyed by: API key / user / tenant (IP as fallback)
  • Pagination caps + field limits + max payload size
  • Predictable 429 responses; clients can back off
  • Quotas/budgets for slow scraping and cost control

Abuse cases + monitoring

  • Top abuse cases documented (scraping, enumeration, stuffing)
  • Audit logs: actor + resource + result + request_id
  • Alerts on spikes, repeated auth failures, export anomalies
  • Replay protection for webhooks (timestamp + signature)
  • Kill switch: blocklist/revoke quickly without deploy

PR review checklist (copy/paste)

  • Does this endpoint expose new data or increase access? (fields, joins, “include=all”)
  • Is authorization checked for the specific resource ID?
  • Is there a cap on pagination, filters, and payload size?
  • What’s the abuse path (scrape/brute force)? What limits stop it?
  • Do logs contain enough signal to investigate without leaking secrets?

Wrap-up

The best API security isn’t a single library or vendor. It’s a set of defaults you apply consistently: verify identity, enforce object-level authorization, cap and throttle what can be abused, and log the signals you’ll need when something goes wrong.

What to do next (15 minutes)

  • Pick your top 3 risky endpoints (login, search, export)
  • Add or tighten route-specific rate limits
  • Confirm object-level authorization exists (and add an IDOR test)
  • Cap pagination and remove “too generous” defaults

What to do next (this week)

  • Document auth + scopes in OpenAPI
  • Centralize authorization policies per resource
  • Set up alerts for abuse signals and export anomalies
  • Add a fast revocation path for tokens/keys
One sentence to remember

If a bot can turn your API into a data pump with valid requests, you don’t have a “hacking problem” — you have an abuse controls problem. Solve it with caps, limits, and visibility.

If you want to go deeper, the related posts below pair well with this checklist: threat modeling, modern auth, DevSecOps, and common real-world attack patterns.

Quiz

Quick self-check (demo). This quiz is auto-generated for cyber / security / api.

1) Which statement best describes authorization (AuthZ) in an API?
2) What is a good reason to add route-specific rate limits (not just a global limit)?
3) Which change most directly reduces scraping risk on list/search endpoints?
4) For webhooks, what combination most effectively prevents replay attacks?