The best time to threat model an API is before the route file exists. At that point the design is still cheap to change, and security decisions can become part of the contract instead of a cleanup task after implementation. I like to start with one question: what would be painful if an attacker could read, mutate, infer, or automate it?

Model the assets, not just the endpoints

A route such as POST /organizations/:id/invites is not the real asset. The assets are the organization membership graph, the invitation token, the audit trail, and the trust decision that says who can introduce a new identity into the workspace. Once those assets are named, the threat model gets sharper.

asset: invitation token
properties:
  confidentiality: token must not appear in logs
  integrity: token must bind to org, role, expiry, and inviter
  availability: invite flow must resist low-cost email flooding
  auditability: acceptance must record actor, source IP, and timestamp

Design authorization as a state machine

Broken object-level authorization often happens when authorization is treated as a single boolean check. For real systems, authorization is stateful: the actor has a role, the resource has an owner, the operation has a risk level, and the target may be in a transitional state. That should become a policy function, not scattered conditionals inside controllers.

canInvite(actor, organization, requestedRole):
  require actor.status == "active"
  require actor.orgId == organization.id
  require actor.role in ["owner", "admin"]
  deny requestedRole == "owner" unless actor.role == "owner"
  require organization.billingState != "suspended"

The important property is that every deny path is explicit and testable. If the policy is central, the team can add property-based tests, fuzz edge states, and review changes without hunting through route handlers.

Write attacker workflows beside user workflows

Product teams usually document the happy path: create invite, email user, accept invite, join workspace. I add attacker workflows beside it: enumerate organization IDs, replay an expired token, downgrade a role in the client, accept an invite after removal, flood invite emails, and trigger logging of secret material.

These are not theoretical. They become acceptance criteria. If an invite is revoked, the acceptance endpoint must fail even if the token signature is valid. If the role is changed in the browser request, the server must ignore it and read the persisted role. If rate limiting exists only at the edge, the service should still have business-level controls for high-risk actions.

Turn the threat model into security tests

A useful API threat model ends with tests developers can run. I keep a small suite that covers horizontal authorization, vertical authorization, replay, input boundaries, sensitive logging, and rate-limit behavior. The suite should be part of CI, but it should also be readable enough for a product engineer to understand the security story.

test("admin cannot invite owner unless they are owner")
test("revoked invite cannot be accepted")
test("invite token never appears in application logs")
test("user from org A cannot read invite metadata for org B")
test("burst invite creation returns 429 and emits security event")

The goal is not to make every engineer a penetration tester. The goal is to make the secure path the obvious path. When the policy, logs, rate limits, and tests are designed before the endpoint ships, the API starts with a security posture instead of acquiring one under pressure.