<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Opula</title>
    <description>The latest articles on DEV Community by Opula (@opula_io).</description>
    <link>https://dev.to/opula_io</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027649%2Ff5e51f97-e179-4dfb-9ce4-079798506446.png</url>
      <title>DEV Community: Opula</title>
      <link>https://dev.to/opula_io</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opula_io"/>
    <language>en</language>
    <item>
      <title>What building a remote MCP server taught me about authentication</title>
      <dc:creator>Opula</dc:creator>
      <pubDate>Mon, 13 Jul 2026 18:11:05 +0000</pubDate>
      <link>https://dev.to/opula_io/what-building-a-remote-mcp-server-taught-me-about-authentication-59m2</link>
      <guid>https://dev.to/opula_io/what-building-a-remote-mcp-server-taught-me-about-authentication-59m2</guid>
      <description>&lt;p&gt;When I started building &lt;a href="https://opula.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_auth" rel="noopener noreferrer"&gt;Opula&lt;/a&gt; - a hosted MCP server that lets Claude read your portfolio, net worth, and cash flow so you can just &lt;em&gt;ask&lt;/em&gt; about your own money - I assumed the hard part would be the finance logic. It wasn't. The hard part was authentication.&lt;/p&gt;

&lt;p&gt;MCP moves fast, the auth story changed twice in 2025, and most of the "add auth to your MCP server" tutorials online are already out of date. So here is the mental model I wish I'd had on day one: how transports shape your auth options, what the current spec actually requires, and the standards (and anti-patterns) that matter in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: auth depends entirely on your transport
&lt;/h2&gt;

&lt;p&gt;MCP defines two standard transports, and they live in completely different security worlds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;STDIO&lt;/th&gt;
&lt;th&gt;Streamable HTTP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Local subprocess&lt;/td&gt;
&lt;td&gt;Remote network service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clients&lt;/td&gt;
&lt;td&gt;1:1 with the host&lt;/td&gt;
&lt;td&gt;N clients, multi-user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network exposure&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth model&lt;/td&gt;
&lt;td&gt;OS process isolation, env vars&lt;/td&gt;
&lt;td&gt;OAuth 2.1, bearer tokens, API keys, mTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-machine&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(SSE was a third, earlier transport. It's deprecated now - if you're starting today, don't.)&lt;/p&gt;

&lt;p&gt;The takeaway: &lt;strong&gt;if your server runs locally over STDIO, you mostly don't need auth.&lt;/strong&gt; The client already trusts the process it spawned, and you pass secrets via environment variables. The official guidance is explicit about this: for STDIO, use environment-based credentials instead of OAuth.&lt;/p&gt;

&lt;p&gt;The moment you go remote over Streamable HTTP - which is what any hosted/SaaS MCP server is - the picture flips. Now you have a single HTTP endpoint (say &lt;code&gt;POST /api/mcp&lt;/code&gt;) reachable over the network, potentially by many users, and you own the entire authentication and authorization story.&lt;/p&gt;

&lt;p&gt;Opula is remote by nature (your Claude talks to &lt;code&gt;https://opula.io/api/mcp&lt;/code&gt;), so everything below is about the HTTP side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec you should actually read: 2025-06-18
&lt;/h2&gt;

&lt;p&gt;The MCP authorization spec was revised in June 2025, and this revision is the one to build against. The single most important sentence in it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A protected MCP server acts as an &lt;strong&gt;OAuth 2.1 resource server&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one line reframes the whole problem. Your MCP server is &lt;em&gt;not&lt;/em&gt; an identity provider. It doesn't own login screens or issue tokens. It &lt;strong&gt;validates&lt;/strong&gt; tokens that some authorization server issued, and it serves protected resources (your tools) when the token checks out.&lt;/p&gt;

&lt;p&gt;The June revision made two changes worth knowing if you read older tutorials:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It &lt;strong&gt;cleanly separated the resource server (your MCP server) from the authorization server.&lt;/strong&gt; You're encouraged to delegate token issuance to a dedicated auth server rather than bolt an OAuth provider onto your tool server.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;removed the old fallback default endpoints&lt;/strong&gt; (&lt;code&gt;/authorize&lt;/code&gt;, &lt;code&gt;/token&lt;/code&gt;, &lt;code&gt;/register&lt;/code&gt;) in favor of mandatory metadata discovery via RFC 9728.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The standards stack
&lt;/h2&gt;

&lt;p&gt;Modern MCP auth is basically four OAuth RFCs wearing a trench coat. Here's the whole cast:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RFC&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 9728&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resource Server&lt;/td&gt;
&lt;td&gt;Protected Resource Metadata - your server advertises &lt;em&gt;which&lt;/em&gt; auth servers to trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 8414&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authorization Server&lt;/td&gt;
&lt;td&gt;AS Metadata - the auth server advertises its endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 7591&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Dynamic Client Registration - clients register themselves without manual setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 8707&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Token audience&lt;/td&gt;
&lt;td&gt;Resource Indicators - tokens are bound to a specific target resource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus &lt;strong&gt;OAuth 2.1 Authorization Code + PKCE&lt;/strong&gt; as the grant flow. Let's walk the flow the way a client actually experiences it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discovery flow, step by step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Client hits your endpoint with no token.&lt;/strong&gt; You reject it with a &lt;code&gt;401&lt;/code&gt; - but not a bare 401. You include a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header pointing at your metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer realm="mcp",&lt;/span&gt;
&lt;span class="s"&gt;  resource_metadata="https://opula.io/.well-known/oauth-protected-resource"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Client fetches your Protected Resource Metadata (RFC 9728).&lt;/strong&gt; This is a tiny JSON document at a well-known URL that says "here's who I am and who issues tokens for me":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://opula.io/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://auth.opula.io"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bearer_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp:tools:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp:portfolio:read"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the piece that older guides skip and the June spec makes mandatory. The client no longer &lt;em&gt;guesses&lt;/em&gt; your auth endpoints - you tell it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Client discovers the authorization server (RFC 8414)&lt;/strong&gt;, optionally registers itself (RFC 7591), and runs a standard &lt;strong&gt;OAuth 2.1 Authorization Code flow with PKCE&lt;/strong&gt;. PKCE is not optional here: MCP clients like Claude Desktop and Cursor are public clients that can't safely store a client secret, so the spec &lt;em&gt;mandates&lt;/em&gt; PKCE for everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Client requests a token bound to your resource (RFC 8707).&lt;/strong&gt; It sends a &lt;code&gt;resource&lt;/code&gt; parameter so the issued token's audience (&lt;code&gt;aud&lt;/code&gt;) is specifically &lt;em&gt;your&lt;/em&gt; server, not "any API this user can reach."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Client retries with the token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/api/mcp&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opula.io&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer eyJhbGciOiJSUzI1NiIs...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you validate and serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the token on the server
&lt;/h2&gt;

&lt;p&gt;Whatever framework you use, token validation on a resource server comes down to the same checks. Here's the shape of it in a Node handler (Opula runs on Node on Vercel):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamableHTTPServerTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/sdk/server/streamableHttp.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Claims&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;jwks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AUTH_SERVER_JWKS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// The checks that actually matter:&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iss&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;TRUSTED_ISSUER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// who issued it&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aud&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://opula.io/api/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// issued FOR us (RFC 8707)&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;nowSeconds&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;                 &lt;span class="c1"&gt;// not expired&lt;/span&gt;
  &lt;span class="nf"&gt;assertScopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;requiredScopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// allowed to do this&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audience check (&lt;code&gt;aud&lt;/code&gt;) is the one people drop, and it's the one that stops the scariest attacks. Which brings me to the mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three anti-patterns I had to design around
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Token passthrough - explicitly forbidden.&lt;/strong&gt; The tempting shortcut is: let the client hand you a token for some downstream API (say a brokerage PAT) and just forward it. The spec says &lt;strong&gt;MUST NOT&lt;/strong&gt;. If you accept tokens that weren't issued &lt;em&gt;for you&lt;/em&gt;, you break audit trails, blur identities in downstream logs, and turn your server into a proxy for stolen tokens.&lt;/p&gt;

&lt;p&gt;The fix is a design rule: &lt;strong&gt;the MCP server is the system of record.&lt;/strong&gt; Clients authenticate &lt;em&gt;to&lt;/em&gt; you with MCP-scoped credentials; you then use your &lt;em&gt;own&lt;/em&gt; managed credentials to call downstream services. Opula calls its market-data sources (like the FRED economic data API) with server-side keys that never touch the client - which, conveniently, is exactly what "no token passthrough" demands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The confused deputy.&lt;/strong&gt; If your server uses a single static client ID to talk to a third-party provider on behalf of every user, that provider may skip the consent screen for a request that &lt;em&gt;looks&lt;/em&gt; trusted. An attacker can ride that to get a user authorized to something they never intended. Mitigation: dynamic client registration and explicit user consent, strict redirect-URI validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Forgetting you're on a network.&lt;/strong&gt; Two cheap but critical HTTP-transport rules from the spec: &lt;strong&gt;validate the &lt;code&gt;Origin&lt;/code&gt; header&lt;/strong&gt; on every connection (prevents DNS-rebinding attacks), require HTTPS in production, and if you ever run locally, bind to &lt;code&gt;127.0.0.1&lt;/code&gt; rather than &lt;code&gt;0.0.0.0&lt;/code&gt;. One more from hard-won ops experience: &lt;strong&gt;scrub the &lt;code&gt;Authorization&lt;/code&gt; header out of your logs.&lt;/strong&gt; It's a bearer token; logging it is the same as logging a password.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pragmatic path: start with bearer, grow into OAuth
&lt;/h2&gt;

&lt;p&gt;Here's the honest part. Full RFC 9728 + 8414 + 7591 + 8707 + PKCE is the destination, not necessarily your day-one commit. Plenty of shipping MCP servers - Opula included - start with a validated &lt;strong&gt;bearer token over Streamable HTTP&lt;/strong&gt;: the client sends &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;, middleware verifies signature, issuer, audience, expiry, and scope &lt;em&gt;before&lt;/em&gt; the request ever reaches the MCP transport layer, and everything not explicitly authorized gets a clean &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That gets you a secure, multi-user server today. The value of understanding the full spec is that it tells you exactly where the seams are when you grow: the day a client wants to self-register, or an enterprise wants its own IdP to issue the tokens, you already know which RFC slots in where instead of re-architecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick your transport first.&lt;/strong&gt; STDIO = local, use env vars, skip OAuth. Streamable HTTP = remote, you own auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your MCP server is an OAuth 2.1 resource server&lt;/strong&gt;, not an identity provider. It validates tokens; it doesn't mint them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery is mandatory now:&lt;/strong&gt; &lt;code&gt;401&lt;/code&gt; + &lt;code&gt;WWW-Authenticate&lt;/code&gt; -&amp;gt; Protected Resource Metadata (RFC 9728) -&amp;gt; auth server -&amp;gt; Authorization Code + &lt;strong&gt;PKCE&lt;/strong&gt; -&amp;gt; audience-bound token (RFC 8707).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always check &lt;code&gt;aud&lt;/code&gt;.&lt;/strong&gt; A token not issued &lt;em&gt;for your server&lt;/em&gt; is a token you must reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never do token passthrough.&lt;/strong&gt; Be the system of record; use your own downstream credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can ship a validated bearer token today&lt;/strong&gt; and grow into the full flow - as long as you know where the seams are.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;MCP Authorization spec (2025-06-18): &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP Security Best Practices: &lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 9728 - OAuth 2.0 Protected Resource Metadata&lt;/li&gt;
&lt;li&gt;RFC 8707 - Resource Indicators for OAuth 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://opula.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_auth" rel="noopener noreferrer"&gt;Opula&lt;/a&gt;, a hosted MCP server that turns Claude into an analyst for your own money. If you're building MCP servers too, I'd love to compare notes on where you drew the auth line.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>oauth</category>
    </item>
  </channel>
</rss>
