How-to cluster: create-mcp-server-without-code, claude-desktop-mcp-setup, chatgpt-mcp-connector (incl. honest Business/Enterprise write-connector plan limits), rest-api-to-mcp-server. Comparison cluster: mcp-server-hosting-pricing (5-platform matrix), composio-alternative, smithery-alternative — house style: explicitly state where competitors win. Technical/GEO cluster: mcp-transports-explained (SSE deprecation 2025-03-26), mcp-oauth-plain-english, mcp-server-security-checklist, and German DACH article mcp-server-ohne-code-erstellen (articleJsonLd gained optional inLanguage param). Every article: pageMetadata canonical, Article JSON-LD with Person author + image + wordCount, BreadcrumbList, FAQPage where applicable, 1200x630 OG image via shared helper, internal linking. Registry entries in lib/articles.ts feed guides index, sitemap and RSS automatically. Product claims sourced only from lib/seo.ts truth — no invented customers, SLAs or certifications. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
181 lines
8.9 KiB
XML
181 lines
8.9 KiB
XML
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>
|
|
</>
|
|
);
|
|
}
|