buildmymcpserver/apps/web/app/(marketing)/guides/mcp-oauth-plain-english/page.tsx

181 lines
8.9 KiB
TypeScript
Raw Normal View History

import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link';
import { ArticleShell, H2, Note, OL, P, Strong, UL } from '../article-shell';
const PATH = '/guides/mcp-oauth-plain-english';
const TITLE = 'OAuth 2.1 for MCP servers: PKCE, DCR and RFC 8707 in plain English';
const DESCRIPTION =
'The three RFCs behind MCP authorization — PKCE, Dynamic Client Registration and Resource Indicators — explained without jargon: what each one does, the full flow step by step, and what breaks when you skip one.';
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
export default function Page() {
return (
<>
<JsonLd
data={articleJsonLd({
title: TITLE,
description: DESCRIPTION,
path: PATH,
datePublished: '2026-07-08',
authorName: 'Marco Sadjadi',
wordCount: 1400,
})}
/>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Guides', path: '/guides' },
{ name: TITLE, path: PATH },
])}
/>
<ArticleShell
title={TITLE}
subtitle="MCP authorization is OAuth 2.1 plus three RFCs that most OAuth tutorials never mention. This is the concept explainer — what each piece does and why the spec requires it. For the deployment how-to, see the hosting guide."
updated="July 2026"
>
<H2>Why MCP needs more than classic OAuth</H2>
<P>
Classic OAuth assumes you know your clients in advance: you register an app in a
dashboard, get a <code>client_id</code> and <code>client_secret</code>, and ship them
inside your application. MCP breaks both assumptions. The client is someone's Claude
Desktop or Cursor install you will never pre-register it and it runs on a desktop
where a baked-in secret is not a secret. OAuth 2.1 plus three extension RFCs is how the
MCP spec resolves this.
</P>
<H2>The two roles: Authorization Server and Resource Server</H2>
<P>
Every OAuth setup splits into two jobs. The <Strong>Authorization Server (AS)</Strong>{' '}
authenticates users and issues tokens. The <Strong>Resource Server (RS)</Strong> your
MCP server accepts requests only when they carry a valid token. They can be the same
deployment or separate services; the protocol only cares that the RS can verify what the
AS signs, typically via a published JWKS (the AS's public keys).
</P>
<H2>PKCE (RFC 7636): proof that the token requester started the flow</H2>
<P>
The authorization-code flow hands the client a one-time code via a browser redirect, and
the client exchanges that code for a token. The classic attack is stealing the code
in-flight and exchanging it yourself. <Strong>PKCE</Strong> closes this: the client
invents a random secret (the verifier), sends only its hash (the challenge) when the flow
starts, and must present the original verifier at exchange time. A thief who intercepted
the code never saw the verifier, so the code is useless to them.
</P>
<P>
In OAuth 2.1, PKCE is <Strong>mandatory for every client</Strong> the old exemption for
"confidential" server-side clients is gone. If your AS treats PKCE as optional, it is not
OAuth 2.1.
</P>
<H2>Dynamic Client Registration (RFC 7591): clients register themselves</H2>
<P>
When a user adds your MCP server to Claude Desktop, the client calls your AS's
registration endpoint at runtime "here is my name and redirect URI" and receives a
fresh <code>client_id</code> on the spot. No dashboard, no support ticket, no shared
credentials between users. <Strong>DCR</Strong> is what makes "paste a URL, connect,
done" possible: without it, every user of every MCP client would need you to manually
provision app credentials.
</P>
<H2>Resource Indicators (RFC 8707): tokens bound to one server</H2>
<P>
A user might connect their client to ten different MCP servers. Without{' '}
<Strong>Resource Indicators</Strong>, a token minted for server A could be replayed
against server B any server you talk to could impersonate you elsewhere. RFC 8707 has
the client name the exact server (the <code>resource</code>) when requesting the token,
and the AS bakes that audience into the token. Your MCP server then rejects any token
whose audience is not itself. One token, one server, no cross-server replay.
</P>
<H2>The full flow, step by step</H2>
<OL>
<li>
The client sends an unauthenticated request to your MCP server and gets a{' '}
<Strong>401</Strong> with a <code>WWW-Authenticate</code> header pointing at the
protected-resource metadata this is how the client discovers your AS.
</li>
<li>The client registers itself with the AS via DCR and receives a client_id.</li>
<li>
The client generates a PKCE verifier + challenge and opens the browser to the AS's
authorize endpoint, naming your server as the <code>resource</code>.
</li>
<li>The user signs in and approves; the AS redirects back with a one-time code.</li>
<li>
The client exchanges code + PKCE verifier for an access token that is
audience-bound to your server.
</li>
<li>
Every subsequent MCP request carries the token; your server verifies signature, expiry
and audience on each call.
</li>
</OL>
<H2>What breaks when you skip a piece</H2>
<UL>
<li>
<Strong>No protected-resource metadata / wrong 401</Strong> clients can't discover
your AS. The symptom: the server "works" with curl but silently fails to install from
Claude Desktop. This is the most common failure in the wild.
</li>
<li>
<Strong>No DCR</Strong> the connect flow dead-ends at "unknown client" for anyone but
you.
</li>
<li>
<Strong>No PKCE (or PKCE accepted but not enforced)</Strong> the flow appears to work
but intercepted authorization codes become exchangeable. Invisible until exploited.
</li>
<li>
<Strong>No audience check (RFC 8707)</Strong> tokens for other servers are accepted
by yours, and yours are accepted elsewhere. Also invisible until exploited.
</li>
</UL>
<Note>
The failure modes split into two families: discovery mistakes are loud (nothing
connects), security mistakes are silent (everything connects, including attackers). Test
for both an end-to-end install from a real client proves discovery, but only negative
tests (expired token, wrong audience, missing PKCE) prove the security half.
</Note>
<H2>Common implementation mistakes</H2>
<UL>
<li>Substring-matching redirect URIs instead of exact-match comparison.</li>
<li>Allowing authorization codes to be exchanged more than once.</li>
<li>Accepting <code>plain</code> PKCE instead of requiring <code>S256</code>.</li>
<li>
Verifying token signature and expiry but forgetting the audience claim RFC 8707 only
protects you if the RS actually checks it.
</li>
<li>
Long-lived access tokens as a substitute for refresh tokens shorter access-token
lifetime plus refresh is the OAuth 2.1 posture.
</li>
</UL>
<H2>Build it or get it built-in</H2>
<P>
None of this is exotic, but it is a genuine authorization-server implementation days of
work plus ongoing spec-tracking, and mistakes are security bugs rather than build
failures. If you want to own it, the{' '}
<Link
href="/guides/host-mcp-server-with-oauth"
className="text-[--color-accent] hover:underline"
>
hosting guide
</Link>{' '}
walks through the deployment options. If you'd rather not: every server generated on
BuildMyMCPServer ships behind an OAuth 2.1 AS with PKCE, DCR and Resource Indicators
already wired the flow above is what our{' '}
<Link href="/docs/oauth" className="text-[--color-accent] hover:underline">
OAuth docs
</Link>{' '}
implement, and the <Link href="/pricing" className="text-[--color-accent] hover:underline">free tier</Link>{' '}
includes it.
</P>
</ArticleShell>
</>
);
}