Building product

MCP OAuth explained: the 401 pointer, PKCE, dynamic registration and scopes, walked end to end

MCP authorization is OAuth 2.1: a 401 points to the server's metadata, the client registers itself, PKCE protects the code, and a scoped.

By Atrix teamPublished 10 min read
On this page
  1. Why MCP uses OAuth at all
  2. The flow, step by step
  3. What each piece protects against
  4. PKCE: why the code is useless to a thief
  5. Dynamic client registration: why nobody creates an app in a console
  6. Scopes: narrowing, never widening
  7. The same flow on a real server
  8. PKCE flow vs pasted token: what you give up
  9. FAQ
  10. Next step

MCP authorization is OAuth 2.1 with a few mandatory pieces. An unauthenticated request to a remote MCP server gets a 401 that points to the server's metadata; the client follows it to the authorization server, identifies itself, opens the browser so the person can consent to scopes, and exchanges a PKCE-protected code for a short-lived access token and a rotating refresh token. The result is a scoped, revocable credential instead of a key pasted into a config file.

That is the whole flow in one paragraph. The rest of this post walks it step by step against the MCP authorization specification (revision 2025-11-25), explains what PKCE, dynamic client registration and scopes each protect against, and shows the flow as it actually runs against a real server, ours, so you can see what each stage looks like from the client's side.

Why MCP uses OAuth at all

A local MCP server over stdio reads its credentials from the environment, and the spec says it should not use OAuth. A remote server is different: one deployment serves many people, each with different permissions, from AI apps that the server operator has never seen. Three problems follow.

  1. The server needs to know who is asking, on every call.
  2. The person needs to limit what the app may do, without handing it everything.
  3. Both need a way to end the access later, without changing passwords or rotating keys by hand.

A pasted API key solves the first problem badly and the other two not at all. OAuth was designed for exactly this shape, an app acting for a person at a service, and OAuth 2.1 is the consolidated, stricter version of it. The glossary entry on OAuth 2.1 lists what it removed from 2.0; for MCP the relevant parts are that PKCE is required, redirect URIs must match exactly, and tokens may not travel in query strings.

The MCP authorization spec builds on five documents: the OAuth 2.1 draft (draft-ietf-oauth-v2-1-13), Authorization Server Metadata (RFC 8414), Dynamic Client Registration (RFC 7591), Protected Resource Metadata (RFC 9728) and the Client ID Metadata Documents draft. You do not need to read them to use an MCP server, but each one answers one of the questions below.

The flow, step by step

Here is what happens between the moment you add a remote server and the moment the model makes its first tool call. The client does all of it; you see a browser tab.

  1. The client calls the server without a token. The server, acting as an OAuth resource server, answers 401 Unauthorized with a WWW-Authenticate: Bearer header containing resource_metadata, a URL. The spec requires servers to implement RFC 9728 and requires clients to parse this header. If the header is missing, the client must try the well-known URIs instead.
  2. The client fetches the protected resource metadata. The document names at least one authorization_servers entry and may list scopes_supported.
  3. The client discovers the authorization server. It fetches that server's metadata via RFC 8414 (/.well-known/oauth-authorization-server) or OpenID Connect Discovery; clients must support both. The metadata lists the authorization, token and registration endpoints, the supported PKCE methods and, if present, client_id_metadata_document_supported.
  4. The client checks PKCE support. If code_challenge_methods_supported is absent from the metadata, the client must refuse to proceed. There is no silent downgrade.
  5. The client identifies itself. Three options, in the spec's priority order: pre-registered credentials if it has them; a Client ID Metadata Document, where the client ID is an HTTPS URL to a JSON file the client hosts; or dynamic client registration, a POST to the registration endpoint that returns a client_id on the spot. The spec now says servers and clients should support metadata documents and may support dynamic registration as a fallback.
  6. The client opens the browser. The authorization URL carries the client ID, the redirect URI, the requested scopes, a state value, the PKCE code_challenge with code_challenge_method=S256, and the resource parameter naming the MCP server (RFC 8707). The person signs in and sees a consent screen listing the client's name and the scopes.
  7. The authorization server redirects back with a code. The code is short-lived and single-use.
  8. The client exchanges the code for tokens. The token request carries the code, the original PKCE code_verifier and the same resource parameter. The response is an access token and, usually, a refresh token.
  9. Every MCP request carries the token in Authorization: Bearer, never in the URL, and the server validates on each call that the token is valid and was issued for this server specifically.
  10. When a call needs more scope, the server answers 403 with error="insufficient_scope" and the scopes required. The client runs the flow again for the wider set; the spec calls this step-up authorization and tells clients to cap retries.

What each piece protects against

MechanismDefined inThe attack it stops
Protected resource metadataRFC 9728Clients guessing or being tricked into using the wrong authorization server
Authorization server metadataRFC 8414Hard-coded endpoints that break when the server changes
PKCE (S256)RFC 7636, OAuth 2.1A stolen authorization code being redeemed by someone else
Exact redirect URI matchingOAuth 2.1Open redirects that send the code to an attacker
state parameterOAuth 2.1Cross-site request forgery on the callback
resource parameterRFC 8707A token for one server being replayed at another
Audience validation on the serverRFC 8707, OAuth 2.1Token passthrough and confused-deputy attacks
Short-lived access tokensOAuth 2.1A leaked token staying useful for long
Rotating refresh tokensOAuth 2.1A stolen refresh token being reused silently
Scopes and consentOAuth 2.0/2.1An app doing more than the person agreed to

Two of these deserve a closer look because they are the ones people ask about.

PKCE: why the code is useless to a thief

Most MCP clients are public clients: a desktop app, an editor or a CLI with no secret it could keep. In the authorization code flow, the code comes back through a redirect, and on a laptop or phone a redirect can be intercepted by another program registered for the same URL. Before PKCE, whoever held the code could redeem it.

PKCE adds a one-time secret per sign-in. The client generates a random code_verifier, keeps it, and sends code_challenge = BASE64URL(SHA256(code_verifier)) with the authorization request. When it exchanges the code, it presents the original verifier. The authorization server hashes it, compares it with the stored challenge, and refuses the exchange if they differ. A thief with the code but not the verifier gets nothing. The PKCE entry covers the plain method and why OAuth 2.1 and MCP say to use S256 whenever possible.

Dynamic client registration: why nobody creates an app in a console

Classic OAuth assumed a developer registered each app in advance. That does not scale to a world where any of thousands of AI clients may connect to any of thousands of MCP servers with no prior relationship. Dynamic client registration (RFC 7591) lets the client POST its own metadata, a name and redirect URIs, to the registration endpoint and receive a client_id immediately.

The 2025-11-25 revision adds a cleaner answer, Client ID Metadata Documents: the client ID is itself an HTTPS URL pointing at a JSON document that describes the client. The authorization server fetches it, checks that the document's client_id matches the URL exactly, and validates redirect URIs against it. The spec says clients and servers should support this and may keep dynamic registration for compatibility. Because any client can register, a careful authorization server treats registration as conveying no trust: it shows the client's name and redirect host plainly on the consent screen and warns when the redirect is a bare localhost port, which the spec notes cannot be fully protected by metadata documents alone.

Scopes: narrowing, never widening

Scopes are the names on the consent screen: work.read, crm.write, and so on. An app asks for a set, the person approves some or all, and the token carries the result. The MCP spec tells clients to request the scopes from the 401 challenge's scope parameter if present, otherwise the server's scopes_supported, and to step up later rather than ask for everything at once.

The rule that matters most is one the OAuth standard leaves to the API: a scope describes what the app may ask for on the person's behalf, and it must never exceed what that person can do themselves. A good server checks both on every call: does the token hold the scope, and is the person behind it allowed to do this to this record?

The same flow on a real server

This is what the steps above look like against the Atrix MCP server at https://atrix.dev/api/mcp, which we run and use daily, so you can match the abstract flow to concrete behaviour. The developer guide has the client-by-client instructions.

In Claude Code, the whole setup is claude mcp add --transport http atrix https://atrix.dev/api/mcp, then /mcp and Authenticate. The command deliberately carries no credential. What happens next:

  • The first request gets 401 with the RFC 9728 pointer. Discovery follows RFC 8414 and RFC 9728.
  • The client registers itself dynamically; there is no client secret anywhere, because every client, including our own apps, is a public client.
  • PKCE is S256 only. The server advertises only S256 in its metadata and refuses plain.
  • The browser opens; you choose a workspace and approve scopes. Codes live 60 seconds and work once.
  • The client receives an access token with the prefix atx_oat_, valid for one hour, and a refresh token with the prefix atx_ort_, valid for 90 days and rotated on every use.
  • Scopes never add to your permissions. They are resolved per call against the account behind the token, so if a teammate's access is revoked, every token they created stops reaching anything on the next request. Nine scopes also require the matching permission on the account, such as deal values or employment records, and pay is not grantable at all.
  • Limits are 120 calls and 30 writes per minute per token. Every call is audited by tool, outcome and subject, never by contents.

For CI jobs and servers with no browser, there is a personal access token instead, prefixed atx_pat_, sent as --header "Authorization: Bearer ...". It works, and it is the worse choice anywhere a browser exists: it lives as plaintext on disk, it is as long-lived as you chose at creation, and it skips the consent screen. We keep that option for pipelines and recommend OAuth for people.

PKCE flow vs pasted token: what you give up

OAuth 2.1 with PKCEPasted personal access token
How you connectAdd URL, approve in browserCreate token in settings, paste into config
LifetimeAccess token 1 hour, refresh 90 days rotating (Atrix)30 days to forever, as chosen
Stored whereClient's secure storeConfig file, often plaintext
Scoped byConsent screen, step-up laterChosen at creation
Revoked byRevoking the grant or the accountDeleting the token
Needs a browserYesNo
Right forPeople using an AI appCI, cron, headless servers

FAQ

Does MCP require OAuth?

Authorization is optional in the protocol. When a server uses an HTTP transport and supports authorization, the spec says it should conform to the OAuth 2.1-based authorization specification. stdio servers should not use it and should take credentials from the environment.

What is the difference between OAuth 2.0 and OAuth 2.1 for MCP?

OAuth 2.1 removes the implicit and password grants, requires PKCE for the authorization code flow, requires exact redirect URI matching, forbids bearer tokens in query strings, and requires refresh token rotation or sender-constraining for public clients. MCP inherits all of these.

Why does my MCP client refuse to connect to an authorization server?

The most common reason is missing PKCE support: if code_challenge_methods_supported is absent from the authorization server's metadata, an MCP client must refuse to proceed. The second is a missing or malformed WWW-Authenticate header on the 401.

Is dynamic client registration still required?

No. The 2025-11-25 revision says clients and servers should support Client ID Metadata Documents and may support dynamic registration as a fallback. Many servers, including Atrix, still offer dynamic registration because older clients rely on it.

Can an MCP token do more than the person who created it?

It should not. Scopes narrow access; they do not add to it. A well-built server resolves the person's permissions on every call, so a token can never reach more than its owner can, and loses access the moment the owner does.

When should I use a personal access token instead of OAuth?

Only where there is no browser: CI pipelines, cron jobs, headless servers. Everywhere else, OAuth gives you a shorter-lived, scoped, revocable credential without storing a secret on disk.

Next step

The developer guide walks the Atrix flow client by client, with the exact commands for Claude Code, Claude Desktop, ChatGPT and Cursor. If a term above was new, the OAuth 2.1 and PKCE entries are the shortest explanations we know of.

Share this article

Share this article

Keep reading

All posts

Put the ideas in this post to work.

Try it for 7 days. A card is required to start and is charged at the end of the trial unless you cancel; cancelling during the trial costs nothing.

7-day trial · card required · cancel anytime