Security 2 Sep 2026 · 5 min read

MCP authentication: OAuth 2.1, API keys, and what the specification actually requires

The spec says OAuth for remote servers and most of the world uses a bearer key. Here is what each arrangement actually does with your credential, where it ends up, and what revoking it costs.

MR mcprush team · The people who run the gateway

"Connect your GitHub" is the same sentence in front of three completely different arrangements, and the difference between them is not security in the abstract. It is a concrete question with a concrete answer: where does the secret end up, and what do you have to do when it leaks? Everything below is that question asked of each way an MCP server can authenticate.

Three ways to hold the same grant. The column that matters is the last one.Three ways to hold the same grant. The column that matters is the last one.
Three ways to hold the same grant. The column that matters is the last one.

Local servers: the environment, and nothing else

A stdio server has no authentication of its own — the client that started it is the only thing that can talk to it. What it needs is a credential for whatever it reaches, and the specification is deliberately silent about how it gets one: the convention is an environment variable, set in the env block of the client’s config entry.

That convention has a consequence people discover late. The key is in a JSON file on disk, in plain text, in every place that file is synced or backed up, and in the process environment of every server the client starts. Rotating it means finding every copy. Revoking it means the same. A local server holding a long-lived key to a third-party service is the most common shape in the world and the worst one: the secret is on your disk like a local server, and the blast radius is somebody else’s production system like a remote one.

  • Name the variables the server refuses to start without, and fail at startup rather than at the first call.
  • Ask for the narrowest scope the tools use. Scopes are copied from a README and never trimmed, and anything that compares them with the tool surface will say so.
  • Never put a key in args. It appears in the process list of the machine, which is broader than the file.

Remote servers: what the specification requires

Once a server is at an address, the specification has an opinion. The 2025-03-26 revision brought OAuth 2.1 to HTTP transports; the 2025-06-18 revision fixed what had been the awkward part by separating the roles: an MCP server is an OAuth resource server, not an authorization server. It does not issue tokens. It publishes, at a well-known address, which authorization server does — and the client goes there.

RequirementStandardWhat it means in practice
Protected resource metadataRFC 9728The server publishes /.well-known/oauth-protected-resource naming its authorization server, and a 401 points at it
Authorization server metadataRFC 8414The client discovers endpoints rather than being configured with them
PKCEOAuth 2.1Required on every authorization code flow, no exceptions for "trusted" clients
Resource indicatorsRFC 8707The client says which server the token is for, so a token minted for one server is useless at another
Dynamic client registrationRFC 7591Recommended, so a client nobody pre-registered can still connect
No token passthroughMCP specificationA server must not forward the token it received to an upstream API; it uses its own credential there
The last row is the one with teeth. A server that proxies your token onward has made every upstream it reaches part of its blast radius, and yours.

What a client does with all of that: it sends a request, gets a 401 that names the resource metadata, reads which authorization server to use, registers itself if it has to, runs the authorization code flow with PKCE in a browser tab, and comes back with a token bound to that one server. The person sees one tab and one consent screen. The token lives in the client’s own store, per machine.

In practice: a bearer key

Most remote servers in the wild, and every gateway, take something simpler: an API key in an Authorization: Bearer header on every request. The specification does not forbid it — a bearer token is a bearer token, and the server is free to decide what mints one — it just does not describe it, because there is nothing to describe. You get a key from a settings page and paste it into a config file.

Which puts the secret back on disk, in the headers block this time rather than env. The difference from the local case is at the other end: a key issued by a service can be listed, scoped, expired and revoked by that service, per key, without touching the account it belongs to. That is a real improvement, and it holds only if the keys are actually issued that way — one per machine, with a scope and an expiry — rather than one long-lived key for everything, copied into every file.

{ "mcpServers": { "issues": { "type": "http", "url": "https://…/mcp", "headers": { "Authorization": "Bearer <a key for this machine>" } } } }
The shape in most clients. VS Code can hide the value behind an input prompt; the others hold it in plain text.
  • One key per machine, not per person. Machines get lost, decommissioned and rotated independently; a key that belongs to a laptop can go with the laptop.
  • A scope on the key — read-only where the work is reading — so that a leaked key cannot reach the tools that write.
  • An expiry. A key that cannot expire is a key somebody will find in a backup in two years.
  • Never in args, never in a URL. Both end up in logs the person who pasted the key never sees.

The brokered arrangement

There is a third shape, and it is the one that removes the secret from your disk altogether. A gateway sits between the client and the server: the client holds a key to the gateway and nothing else, and the gateway holds the credential for the upstream — the OAuth grant, the API key, whatever the service takes — on your behalf. Your config file contains one key, for one gateway, with one scope. The upstream credential never reaches your machine.

The middle box is where a brokered credential lives. It is also where a log, an allowance and a revocation can live, which is why the shapes tend to come together.The middle box is where a brokered credential lives. It is also where a log, an allowance and a revocation can live, which is why the shapes tend to come together.
The middle box is where a brokered credential lives. It is also where a log, an allowance and a revocation can live, which is why the shapes tend to come together.

What that buys is revocation that means something. "Revoke this one integration" is one action at the gateway and changes nothing else; "revoke this machine" is one key, and every other machine keeps working. What it costs is a third party in the path of every call, which is either fine or unacceptable depending on what the calls carry, and that is a decision to make on purpose rather than by default.

The three shapes, side by side

ArrangementWhere the secret livesRevoking it meansA leak costs
Key in a local server’s environmentA config file on disk, and everywhere it syncsRotate at the service, then find every fileWhatever the key’s scope reaches, until you notice
OAuth, held by the clientThe client’s own store, per machineRevoke the grant, per machineOne machine’s grant, until revoked
Bearer key to a remote serverA config file on disk, per machine if issued that wayRevoke one key at the issuerOne key’s scope on one machine
Brokered through a gatewayWith the gateway; never on your diskOne action, one integration or one machineAccess to the gateway, not the upstream credential
None of the four is wrong. The first one is the one most people have, and the only one where the honest answer to "what do we do when it leaks" is an inventory nobody trusts.

Reading a refusal

When a call fails after all of this, the status code says which half of the arrangement failed. A 401 is about the credential itself — missing, expired, revoked, unknown — and nothing else; a server that wants OAuth answers it with the metadata pointer above, and a server that wants a key answers it with a sentence. A 403 means the credential was fine and something about what it may do was not: a scope it lacks, a tool that writes called with a read-only key, a plan that lapsed. Debugging a 403 by rotating the key is the most common wasted hour in this whole area.

The checklist

  • Before pasting any key: what scope, what expiry, and is it this machine’s or everybody’s?
  • Before installing a local server with a credential: what does that credential reach beyond what the tools use?
  • Before trusting a remote server with OAuth: does its 401 point at real resource metadata, and does it promise not to pass your token upstream?
  • Before choosing a gateway: what can it see, what does it log, and can you revoke one integration without touching the rest?
  • After any of them: can you list, in under two minutes, every credential your agents hold and where each one is stored? If not, the arrangement is whichever one is hardest to inventory, and that is the first one.

The specification got the remote case right in 2025 by making the server a resource server rather than a bank. The rest is arrangement, not protocol — and the arrangement is decided by whoever pastes the key, which is the one part no specification can fix.