mcp-auth-template
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-auth-templateauthenticate with client_credentials and list tools"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-auth-template
A secure remote Model Context Protocol server template. Streamable HTTP transport, OAuth 2.1, Client ID Metadata Documents, and constant-time credential verification — with a mock authorization server so the whole flow runs on your laptop with no external accounts.
▶ Live demo: mcp-auth.coreframe-labs.dev/demo/ — run the real auth flow in your browser and watch each defence accept or reject a request.
Most MCP servers today run over stdio on localhost with no authentication at all. The moment one becomes remote, it needs a real authorization story. This template is that story, built to be read: every security decision has a comment explaining why, including the places where a defence is deliberately not applied.
Scaling your company's AI integrations? Coreframe Labs helps venture-backed teams and enterprises build secure, production-grade custom software architectures. Let's design your agent infrastructure safely — talk to our Core Architects →
Why this exists
AI-assisted development produces code that passes its tests and still ships security holes, because tests assert behaviour and attackers exploit properties tests never check. Three that recur:
Naive credential comparison.
if (token === expected)returns as soon as two bytes differ. That difference is measurable, and it turns a secret into something an attacker can learn a byte at a time rather than guess whole.Secrets stored in recoverable form. A leaked config file or
docker inspecthands over working credentials.Endpoints that answer questions they were not asked. A token endpoint that responds faster for an unknown client than a wrong password is an enumeration oracle for your customer list.
None of those fail a unit test. All three are addressed here explicitly, and the reasoning is in the code.
Related MCP server: Model Context Protocol Template
What's implemented
Capability | Status |
MCP Streamable HTTP transport (POST/GET/DELETE, SSE, sessions) | official |
OAuth 2.1 | yes |
| yes |
| yes, constant-time |
Client secrets stored as scrypt hashes | yes |
Client ID Metadata Documents + SSRF hardening | yes |
RFC 8707 resource indicators (audience-bound tokens) | yes |
RFC 9728 protected resource metadata | yes |
Client assertion replay prevention ( | yes, in-memory |
Scope narrowing at the authorization server | yes |
Session ownership binding | yes |
Interactive demo with tamper scenarios | yes |
| no — returns 501 |
Token persistence, revocation, distributed state | no |
Quick start — 3 steps
Requires Node 22+. No database, no Docker, no accounts, no .env.
npm install # 1. install
npm run dev # 2. run the all-in-one demo (:3000)
open http://localhost:3000/demo/ # 3. open itThat's the whole thing. npm run dev boots the authorization server, the MCP
resource server and the interactive UI in one process. Click through the
scenarios — each runs the real flow against the server and shows the actual
HTTP exchange:
Scenario | Outcome |
Valid client, correct everything | token issued, tools callable |
Assertion signed with an unpublished key | 401 at the token endpoint |
Assertion addressed to a different token endpoint | 401 at the token endpoint |
Expired assertion | 401 at the token endpoint |
The same assertion presented twice | second exchange rejected |
Valid token missing | 403 at the MCP server |
Token with an edited payload | 401 at the MCP server |
No | 401 + |
Prefer not to install anything? The same UI is live at mcp-auth.coreframe-labs.dev/demo/.
Advanced — run the pieces separately
npm run dev:as # mock authorization server on :4000
npm run dev:rs # MCP resource server on :3000 (needs the AS running)
npm run demo # scripted client against them, prints every stepConfirm the boundary is closed:
curl -i -X POST http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'You get a 401 whose WWW-Authenticate header names the metadata document.
That is not a failure — it is how a compliant MCP client discovers where to get
a token.
You cannot complete the full flow with
curlalone: a client must publish a metadata document at its ownclient_idURL for the authorization server to fetch.npm run demo(and the browser demo) stands one up for you.
Verify everything
npm test # 132 tests
npm run typecheck
npm run buildConstant-time credential verification
The template compares secrets in constant time where a secret is actually being compared, and documents where it deliberately does not. Both halves matter — blanket application is cargo cult and hides where the real boundary is.
Where it is used
src/auth/client-secret.ts — OAuth client secrets. The primary path.
const candidate = await scrypt(presented, parsed.salt, parsed.key.length, params);
return timingSafeEqual(candidate, parsed.key);Two independent properties:
Secrets are never stored recoverably.
CLIENT_SECRETSholds salted scrypt derivations. A leaked config yields no working credential.Comparison is constant-time. It runs over fixed-width derived keys, so secrets of differing lengths compare safely —
crypto.timingSafeEqualthrows on length mismatch, and the obvious guard (if (a.length !== b.length) return false) leaks the secret's length.
Unknown clients are verified against a decoy hash, so "no such client" costs the same as "wrong secret". Without that, the endpoint enumerates your client list by latency alone. There is a test asserting both responses are byte-identical.
src/util/safe-compare.ts covers the dev/CI static-token mode, HMAC-ing both
sides under a per-process random key before comparing.
Where it is deliberately not used
JWT signature verification.
josealready does this correctly. Reimplementing it here would be a downgrade, not an improvement.Issuer, audience, scope,
client_id. None are secrets. A timing oracle on a value the attacker already supplied reveals nothing.
What does more work than constant-time comparison in this codebase is uniform
failure responses: expired, wrong-audience and bad-signature all collapse to
one invalid_token message, so probing yields no signal. A test asserts the
responses are byte-identical.
Registering a client secret
npm run hash-secret -- https://client.example/idPrints the secret once (give it to the client) and the hash (put it in
CLIENT_SECRETS). The server refuses to start if it finds a plaintext value.
Client ID Metadata Documents
Instead of pre-registering clients, a client_id is an HTTPS URL serving a
document describing the client:
{
"client_id": "https://client.example/mcp-client.json",
"client_name": "Example Client",
"token_endpoint_auth_method": "private_key_jwt",
"grant_types": ["client_credentials"],
"scope": "mcp:tools",
"jwks": { "keys": [{ "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }] }
}The validation flow
sequenceDiagram
autonumber
participant C as MCP Client
participant AS as Authorization Server
participant CIMD as Client's Metadata URL
participant RS as MCP Resource Server
C->>AS: POST /token (client_credentials + private_key_jwt)
Note over AS: the assertion's iss claim IS the client_id (an https URL)
AS->>AS: allowlist + SSRF guards on client_id
AS->>CIMD: GET client_id
CIMD-->>AS: metadata document (jwks, auth method)
Note over AS: document.client_id must equal the fetched URL
Note over AS: no redirects, no private IPs, size and type limits
AS->>AS: verify assertion signature vs jwks — check aud, exp, jti
AS-->>C: access_token (aud = resource, scope narrowed)
C->>RS: POST /mcp (Authorization: Bearer TOKEN)
RS->>RS: verify JWT iss, aud, scope, signature via JWKS
RS-->>C: 200 JSON-RPC result — or 401 / 403This makes the authorization server an HTTP client pointed at an
attacker-supplied URL — a textbook SSRF sink. src/auth/cimd.ts enforces:
HTTPS only (relaxed solely for loopback dev via
CIMD_ALLOW_INSECURE)no fragment, no embedded credentials in the URL
DNS resolution checked against loopback, RFC 1918, link-local (including the
169.254.169.254cloud metadata address), CGNAT, multicast, IPv6 equivalentsredirects refused outright rather than re-validated per hop
application/jsonrequired; 64 KiB streamed cap; 5s timeoutdocument.client_idmust equal the URL it was fetched from — otherwise any host could vouch for another host's identityTTL cache over successes and failures; negative caching stops a hostile
client_idbecoming a traffic amplifieran optional allowlist, which any publicly reachable deployment must set — open resolution means anyone can make your server fetch a URL and report whether it worked
Known limitation: DNS rebinding
The guard resolves the hostname, then fetch resolves it again. An entry that
changes between those lookups defeats it. Closing this needs IP pinning with a
custom agent, or an egress allowlist proxy. Not implemented here — documented
rather than hidden.
Session security
Streamable HTTP sessions are identified by an Mcp-Session-Id header, which is
effectively a bearer credential for an already-authenticated stream. Each session
records the client that opened it; presenting someone else's session id returns
404 — identical to a nonexistent session, so the status code cannot enumerate
live sessions.
Configuration
Environment-driven, validated by Zod at boot (src/config.ts). Invalid config
fails immediately and prints every problem at once, naming the offending
variables but never their values — those are exactly where secrets live.
See .env.example for every option. There are no credentials in this repository;
the mock AS generates an ephemeral ES256 keypair per boot.
Deploying
The demo (Railway / Render)
src/demo/index.ts serves the AS, the MCP server and the demo UI on one origin.
docker build -t mcp-auth-demo .
docker run -p 3000:3000 -e PUBLIC_URL=https://your-domain mcp-auth-demoRailway: point it at this repo (railway.json is included), set PUBLIC_URL to
the public origin, then add a CNAME for your subdomain. Render: use the included
render.yaml.
PUBLIC_URL must be the externally reachable origin. It becomes the OAuth
issuer, the RFC 8707 resource identifier, and the client_id URLs the AS
dereferences. Wrong value → every token fails its audience check. The server
refuses to start on a non-HTTPS PUBLIC_URL outside loopback.
Single instance only. Sessions and the jti replay guard are per-process;
a second replica splits them and produces intermittent failures.
Vercel: not without changes
Blocker | Effect |
Sessions in an in-memory | invocations do not share memory; follow-up requests 404 |
SSE streams held open | functions have a hard max duration |
Mock AS mints an ephemeral keypair per boot | each cold start publishes different JWKS; tokens fail intermittently |
| replay protection silently stops working |
Stateless mode (sessionIdGenerator: undefined) plus a real authorization
server would work. For the full stateful behaviour, use a container host.
Security posture
CI runs typecheck, tests, build,
npm audit, gitleaks secret scanning and CodeQL (security-extended) on every push and weekly.No credentials in the repository;
.envis gitignored,.env.exampleis not.CLAUDE.mddocuments the conventions that keep this true.
The mock authorization server is a mock
In-memory state, ephemeral keys, no persistence, no revocation, no rate limiting
on the standalone entrypoint. It exists so the flow runs locally. Do not deploy
it as a real authorization server. For production, point MCP_ISSUER_URL and
MCP_JWKS_URI at a real one; the resource server does not care who issued the
token as long as it verifies.
Dependency advisory
npm audit reports a moderate advisory (GHSA-frvp-7c67-39w9) against
@hono/node-server, transitively via @modelcontextprotocol/sdk: a path
traversal in serve-static on Windows. This project serves no static files
through Hono, so the path is unreachable. npm audit fix --force would downgrade
the SDK to 1.24.3, judged the worse trade. CI fails on high/critical only.
Revisit when the SDK bumps its dependency.
Project layout
src/
app.ts MCP resource server (Express)
config.ts Zod-validated environment config
auth/
cimd.ts CIMD resolution + SSRF hardening
client-authentication.ts parsing of RFC 6749 client auth
client-secret.ts scrypt hashing + constant-time verification
verifiers.ts access token verification (JWT + static)
mcp/server.ts tool definitions
mock-as/
app.ts authorization server
verify-client.ts proves client identity per mechanism
demo/
server.ts all-in-one demo (AS + RS + UI)
scenarios.ts the eight runnable scenarios
page.ts self-contained demo UI
public-url.ts robust PUBLIC_URL resolution
util/safe-compare.ts constant-time string comparison
test/ 132 tests
scripts/
demo-client.ts scripted end-to-end client
hash-secret.ts client secret generationWork with Coreframe Labs
Scaling your company's AI integrations? We help venture-backed teams and enterprises build secure, production-grade custom software architectures. Let's design your agent infrastructure safely.
Licence
MIT — see LICENSE. Use it, fork it, ship it.
This server cannot be deployed
Maintenance
Related MCP Connectors
- StytchOAuthdev.stytch.mcp
The Stytch MCP server is a reference implementation that demonstrates remote MCP server authentication and authorization using Stytch Connected Apps. It provides OAuth 2.1-compliant authorization (including PKCE), Dynamic Client Registration, and validates Stytch-issued access tokens to enable AI agents to securely interact with external services through permissioned access, supporting scopes like openid, email, profile, and manage:project_data.
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
MCP server teaching AI agents to implement TideCloak: auth, E2EE, IGA, security analysis
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA comprehensive Model Context Protocol server template that implements HTTP-based transport with OAuth proxy for third-party authorization servers like Auth0, enabling AI tools to securely connect while supporting Dynamic Application Registration.28 npm7MIT
- AlicenseNot gradedqualityDmaintenanceA production-ready MCP server template that connects LLMs and AI agents to external data, tools, and services with built-in OAuth 2.1 authentication, Redis-backed session management, and a modular tools engine.1MIT
- FlicenseNot gradedqualityCmaintenanceRemote MCP server with built-in OAuth authentication via Cloudflare Access, enabling secure tool usage like math and image generation with user-based access control.-
- AlicenseNot gradedqualityCmaintenanceA production-ready MCP server template with OAuth 2.1, RBAC, and audit logging for building secure, observable tool servers.MIT