Writing

Build the Authorization Model Before You Have Anything to Protect

Build the Authorization Model Before You Have Anything to Protect - abstract illustration

The MCP server at the core of the app I'm rewriting my own infrastructure as got a full authorization model, API keys scoped by domain, operation, and resource, before it had a single tool worth protecting. The only tools registered at that point were time conversion and UUID generation. Building permissions for that felt premature. It wasn't, and the reason why is the part worth writing down.

Retrofitting authorization is how holes happen

The instinct to defer authorization is reasonable on its face: you don't know the final shape of your tool surface yet, so why build the access-control layer before you know what it needs to control. The problem is what actually happens when you defer it. Tools accumulate first, each one reachable by any valid key because there's no gate yet, and "add authorization" becomes a retrofit against a system that's already grown fifty call sites assuming an open door. Retrofitting means finding every one of those call sites, reasoning about what it should have required all along, and hoping you didn't miss one. A missed one isn't a bug you'll catch in code review. It's a tool that quietly stayed unauthorized, discovered later by whoever happens to notice, which in the worst case is whoever exploits it first.

Building the model early costs you a different kind of uncertainty: you have to design grants for tools that don't exist yet. That uncertainty is the cheaper one, because a permission model that's wrong in the abstract gets corrected against the very first real domain it meets, at zero cost to anything already shipped. A permission model that doesn't exist yet gets corrected against everything already shipped, all at once, under pressure.

The shape: domain, operation, resource

A grant in this system is three things: a domain, like filesystem or email or calendar; an operation, like read, write, or delete; and a resource selector, a specific named root, a specific email account, a specific calendar collection. A key can be issued with the full-access owner grant, or with something as narrow as "read-only, this one named filesystem root, nothing else." The API key record itself stores only a hash of the key, never the plaintext, the same discipline you'd want for a password, because a key is a credential and credentials get compromised.

Enforcement happens in three layers, and all three matter independently. First, hasAnyGrant: does this key have any access to this domain at all, which is what determines whether a tool even shows up in the list a client sees. Second, isPermitted: given a specific resource named in a specific call, does this key's grant actually cover it. Third, filterResources: when a tool would otherwise return results spanning multiple resources, like a search across every configured filesystem root, narrow the results to only the roots this key can see before they're returned, not after. Skipping that third layer is a specific, easy-to-miss mistake: a key correctly denied on a direct call to a restricted resource can still leak that resource's existence, or its contents, through a broader search tool that wasn't taught to filter. All three layers, checked independently, close that gap.

Grant-free tools were the right place to start

Time conversion and UUID generation don't need scoping, there's no resource to scope them to, which is exactly why they were the right first tools to build the authorization layer against. The model got proven, the key issuance and revocation flow got built, the "second key with narrower grants sees a smaller tools list" behavior got verified end to end, all before a single sensitive domain existed to get it wrong on. By the time filesystem tools, the first domain with real resources to protect, landed, the authorizer wasn't new code being trusted for the first time under load. It was existing, tested infrastructure that a new domain plugged into, the same relationship every domain since has had to it.

Where the model had to bend

The system stayed uniform until S3 storage, and S3 is where the shape had to change. Every other domain scopes grants to a name configured on the app: a filesystem root you named, an email account you added. S3 buckets aren't a closed, app-known set the same way. An account's own bucket allowlist is the outer bound, but a grant on top of that names bucket strings directly, checked against the one bucket a given call actually requests rather than filtered against an enumerated list, because there's no finite list to enumerate against when an account's allowlist is itself open-ended. It's the one domain, out of seven, where the general "resource selector matches a configured name" pattern doesn't fit, and it needed its own variant of the enforcement check to say so honestly instead of forcing a square peg through the same round hole every other domain used.

That's the real payoff of building the model before the tools existed to test it against: when a genuinely different domain showed up, the question wasn't "how do I retrofit this into the framework." It was "the framework already has a place for this to be different, does that place fit," which is a much smaller, much safer question to answer.

The general version

This generalizes past MCP servers and past this one app. Any system where multiple callers, human or automated, will eventually need different levels of access to different things benefits from the access-control model existing before the surface it will govern does. The uncomfortable part is that you have to design it against imagined future call sites, which feels like guessing. It is guessing, a little. But a permission model that's slightly wrong on day one and gets refined against real domains as they land is a solvable problem. A permission model that doesn't exist on the day your first sensitive tool ships is a security incident with a due date you don't know yet.

Building something like this?

This is the kind of work I do for clients. Tell me what you're building and I'll give you a straight read on the approach.

Book a 30-minute call