Skip to content

oauth for remote mcp servers

nate
Jul 9, 20266 min read1 read

oauth for remote mcp servers

hen an mcp server runs on your own machine, auth is a non-problem: the process reads your env vars and config files, so it acts as you automatically. the moment the server moves to a url, that disappears. now every request arrives over http from who-knows-where, and the server has to answer two questions on each one: who is this, and what are they allowed to touch?

mcp adopted oauth 2.1 to answer them. these notes are what i actually needed to understand to ship one (the prefect cloud mcp server — case study at the bottom).

who's involved

three parties, and keeping them straight explains most of the design:

  • the client is the ai app making tool calls (claude code, some agent runtime).
  • the authorization server is the thing that knows who users are and issues tokens. usually the platform's existing login system.
  • the resource server is the mcp server itself. it receives a token on each request and has to decide whether to honor it.

the crucial point: the mcp server only ever checks tokens. it must not be able to create them. if it could, then anyone who breaks into the mcp server can impersonate anyone on the whole platform. a lot of what follows is just working out the consequences of that one rule.

how a client gets a token

there are two kinds of clients, and each gets its own flow:

a human is present → the browser flow. the app opens a browser, you log in, you approve access, and the app receives a token. mcp clients also use "dynamic client registration," which just means any app can introduce itself to the authorization server on the fly instead of being manually registered ahead of time — necessary because there are thousands of mcp clients, not three.

no human is present → service accounts. a bot or scheduled job holds a client id and secret, and trades them directly for a short-lived token. no browser involved.

either way, the client ends up sending Authorization: Bearer <token> on every request.

how the server knows a token is real

a token is a signed statement from the authorization server: "this bearer is nate, until 3pm, for these things." the mcp server needs to check the signature. there are two ways, and one of them is a trap:

  • shared secret (the trap): the authorization server signs with a secret key, and you give a copy of that key to the mcp server so it can verify. but anyone holding that key can also sign — so you've just handed the mcp server (and anyone who compromises it) a token-printing machine. this violates the check-but-never-create rule.
  • public/private keys (the right way): the authorization server signs with a private key and publishes the public half at a well-known url (a "jwks" endpoint — json web key set). the mcp server fetches the public keys and can verify anything but forge nothing. this is exactly how "sign in with google" works, so the machinery is standard.

we learned this the embarrassing way: the first prefect deploy shipped a copy of cloud's signing secret to the mcp runtime just to verify tokens. it worked, it felt wrong, and the fix was moving to public-key tokens with a jwks endpoint.

two practical details that matter once you're on public keys:

  • tokens name their destination. at issuance, the token records which mcp server it's for (its url becomes the token's "audience"). a token stolen from one server is then worthless at another. the spec for this is rfc 8707, and mcp requires it.
  • publish two keys at once. when you rotate signing keys, old tokens are still in flight. keeping current + next key both in the jwks means rotation doesn't invalidate anyone mid-session.

limiting what a token can reach

the classic consent screen asks yes or no. for a platform where one account can reach many workspaces / orgs / projects, yes-or-no is too blunt — i want to let an agent into two workspaces without letting it into the account.

so the consent step becomes a picker: the user selects which subset of their stuff this grant covers, and the grant remembers the selection. things that fall out of this:

  • enforce it on the platform side, not in the mcp server. the platform's api edge checks every request against the stored grant. the mcp server is just another caller — if it's buggy or compromised, the fence still holds.
  • tool calls must say which tenant they mean. so workspace-scoped tools grow a workspace_id parameter. making it optional keeps the local-mode experience unchanged (there, the ambient config already implies one).
  • agents need a way to ask "what was i granted?" — a list_authorized_workspaces-style tool. it's how "check the prod workspace" in natural language resolves to an actual id, and it's the first call a well-behaved agent makes.
  • "who am i" must work before any workspace is chosen, by describing the grant itself. and for service accounts there may be no user to describe — the prefect /me/ endpoint rightly rejects machine tokens, so identity falls back to describing the grant.

smaller things that bit us

  • if the oauth-enabled entrypoint is missing its verification config, it should refuse to start rather than start unprotected.
  • you don't need a separate codebase for the hosted server. the local and hosted servers share the entire tool surface; the hosted entrypoint just wraps it in token verification.
  • mid-deploy, tokens and half-finished oauth handshakes from the old code are still live. new required fields go into the database as nullable, and stale in-flight state should be rejected with a clear 400, not a stack trace.
  • keep the jwks url public even if interactive login is feature-flagged off — service accounts still need it to verify anything.
  • test the oauth plumbing with unit tests; save evals for user-visible behavior (e.g. "two workspaces are both named prod under different accounts — does the agent disambiguate before reading data?"). assert that the agent consulted its grant, not the exact sequence of tool calls.

case study: prefect cloud mcp

shipped june–july 2026, not yet battle-tested:

sources

  • mcp authorization spec
  • rfc 8707 (resource indicators), rfc 7591 (dynamic client registration), rfc 9728 (protected resource metadata)

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions