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>
    </>
  );
}
