built-in-authorization-server

built-in-authorization-server

An MCP authorization server implementing OAuth2/OIDC flows, supporting dynamic client registration, PKCE, consent screens, and client ID metadata documents for MCP authorization.

Category
访问服务器

README

built-in-authorization-server

Install & run

npm install
PORT=3005 MCP_FAKE_AUTH=1 MCP_FAKE_IDP=1 npm start

Server URL is http://localhost:3005; apiPrefix is auth.

Two modes

REAL mode

Provide real provider credentials in the environment and the real GitHub provider is wired exactly as documented:

GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy JWT_SECRET=<32+ chars> PORT=3014 npm start

The interactive browser leg (/auth/authorize → GitHub → /auth/callback) requires a live GitHub App and is only exercised in this mode.

FAKE mode (offline, MCP_FAKE_AUTH=1)

When MCP_FAKE_AUTH=1 is set and no real credentials are present, dummy clientId/clientSecret are supplied so the module constructs without ever contacting an IdP. Every offline-reachable feature works:

  • Discovery: /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource
  • Dynamic Client Registration (RFC 7591): POST /auth/register (deprecated in MCP revision 2026-07-28, still fully supported here)
  • PKCE required and advertised (code_challenge_methods_supported: ["S256"])
  • JWT validation of locally-signed tokens by the /mcp middleware
  • Guarded MCP calls with a locally-minted JWT

Mint a local token (signed with the same jwtSecret, so validateToken accepts it without any IdP call):

TOKEN=$(npx ts-node scripts/mint-jwt.ts)
bunx @modelcontextprotocol/inspector --cli http://localhost:3014/mcp \
  --transport http --header "Authorization: Bearer $TOKEN" --method tools/list

With MCP_FAKE_AUTH=1 alone the authorize → callback interactive leg still needs the external IdP and is not runnable. Add MCP_FAKE_IDP=1 (below) to get an offline stand-in for it.

What was verified (FAKE mode)

  • Discovery JSON shape matches the doc.
  • POST /auth/register returns a registered client (client_id).
  • PKCE methods advertised.
  • /mcp accepts a valid locally-minted JWT (whoami → Hello, Ada Lovelace!), rejects missing / malformed / wrong-secret tokens with 401.
  • disableEndpoints disables a discovery route (→ 404) while keeping the other.
  • @McpUser() projects req.user.

Tier 4 walkthrough: consent screen and Client ID Metadata Documents

Three opt-in environment flags, all default off. With none of them set this example behaves exactly as it did before they existed — same routes, same metadata document, same GitHub provider, same log output.

Flag Effect
MCP_CONSENT=1 Turns on the interactive consent screen (consent: { enabled: true }).
MCP_CIMD=1 Accepts URL-shaped client_ids, and hosts GET /client-metadata.json + GET /demo-callback so the client side of the demo runs locally too. Implies consent — McpAuthModule forces it on.
MCP_FAKE_IDP=1 ⚠️ Replaces GitHub with a local stub that authenticates everyone as "Ada Lovelace", with no network call. Implies MCP_FAKE_AUTH=1.

FAKE-mode shortcuts, stated plainly

Everything below except MCP_FAKE_IDP=1 is real production behaviour. Two things are shortcuts and neither is safe outside a demo:

  1. MCP_FAKE_IDP=1 — there is no authentication. A real deployment sends the user to GitHub/Google/Keycloak here. It exists only so the browser leg (and therefore the consent screen) can be walked through offline.
  2. allowInsecureClientIdScheme: true, which MCP_CIMD=1 sets — this accepts http:// client_id URLs and disables the SSRF guard, which is the only reason http://localhost:3014/client-metadata.json works as a client_id. With it on, an unauthenticated caller can make this server fetch e.g. https://169.254.169.254/... (the cloud instance-metadata endpoint). In production the document must live on a public https origin and this option must stay false.

A. Consent screen only

PORT=3014 MCP_FAKE_IDP=1 MCP_CONSENT=1 npm start

Register a client and start an authorization request in a browser:

CLIENT_ID=$(curl -s -X POST http://localhost:3014/auth/register \
  -H 'content-type: application/json' \
  -d '{"client_name":"DCR Consent Demo","redirect_uris":["http://127.0.0.1:33418/callback"]}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["client_id"])')

echo "http://localhost:3014/auth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback&code_challenge=WMlOi-4SP-ouuMs3uOb7jhkkMKK5SSJ9Z8Eagvs1wmk&code_challenge_method=S256&state=demo-state"

Open that URL. The fake IdP bounces straight back to /auth/callback, which now answers with the consent screen instead of redirecting. It shows:

  • the client name, and who you are signed in as ("Ada Lovelace"),
  • 127.0.0.1 — the redirect URI hostname, as its own prominent field (draft/basic/authorization/security-considerations makes displaying this a MUST for a CIMD-capable server),
  • an orange loopback warning, because the code will be delivered to a port on this machine that any local process could have bound (a SHOULD),
  • the scopes that will actually be granted — already narrowed, so you see what you are approving rather than what was requested.

Click Approve and the browser lands on http://127.0.0.1:33418/callback?code=…&state=demo-state&iss=http://localhost:3014. Nothing is listening on 33418, so the browser shows a connection error — the code in the address bar is the point. Click Deny instead and you get ?error=access_denied&… on the same URI (RFC 6749 §4.1.2.1).

The code_challenge above is the S256 hash of the verifier sSQKjtmhf3TsxI14yDaGqZpyHg7l1hzoYGMHvRtknJQ, so you can redeem the code:

curl -s -X POST http://localhost:3014/auth/token \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode "code=<the code from the address bar>" \
  --data-urlencode code_verifier=sSQKjtmhf3TsxI14yDaGqZpyHg7l1hzoYGMHvRtknJQ \
  --data-urlencode redirect_uri=http://127.0.0.1:33418/callback \
  --data-urlencode "client_id=$CLIENT_ID"

Approving is remembered for the same (user, client, scope) triple, so a second authorization request goes straight through with no screen. Start the server with MCP_CONSENT=1 and consent.rememberForMs: 0 in main.ts to prompt every time.

B. Client ID Metadata Documents

PORT=3014 MCP_FAKE_IDP=1 MCP_CIMD=1 npm start

The server now advertises support, and hosts a document as if it were the client:

curl -s http://localhost:3014/.well-known/oauth-authorization-server \
  | grep -o '"client_id_metadata_document_supported":true'

curl -s http://localhost:3014/client-metadata.json
{
  "client_id": "http://localhost:3014/client-metadata.json",   // MUST match this URL exactly
  "client_name": "CIMD Demo Client",
  "client_uri": "http://localhost:3014",
  "redirect_uris": [
    "http://localhost:3014/demo-callback",                     // a viewable stand-in
    "http://127.0.0.1:33418/callback"                          // what a real MCP client uses
  ],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"                          // CIMD clients are public
}

There is no registration step. The URL is the client_id. Open:

http://localhost:3014/auth/authorize?response_type=code&client_id=http%3A%2F%2Flocalhost%3A3014%2Fclient-metadata.json&redirect_uri=http%3A%2F%2Flocalhost%3A3014%2Fdemo-callback&code_challenge=WMlOi-4SP-ouuMs3uOb7jhkkMKK5SSJ9Z8Eagvs1wmk&code_challenge_method=S256&state=demo-state&scope=offline_access

The server fetches the document, validates it, and shows the consent screen — with one extra sentence in the loopback warning, because a name that came from a document anyone may reference deserves less trust than a registered one:

Authorization code will be sent to
  localhost
  http://localhost:3014/demo-callback

This client receives the authorization code on this computer.
localhost is a loopback address, so any program running locally could have
opened that port and be impersonating CIMD Demo Client, whose name and logo
come from a document that anyone may reference. Approve only if you just
started this application yourself.

Client identifier  http://localhost:3014/client-metadata.json
Client website     http://localhost:3014
Requested access   offline_access

Click Approve and this time the browser lands on a page that shows you the code, state and iss, because /demo-callback is served by this example. Redeem it with the same URL as client_id (and no secret — it is a public client):

curl -s -X POST http://localhost:3014/auth/token \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode "code=<the code>" \
  --data-urlencode code_verifier=sSQKjtmhf3TsxI14yDaGqZpyHg7l1hzoYGMHvRtknJQ \
  --data-urlencode redirect_uri=http://localhost:3014/demo-callback \
  --data-urlencode client_id=http://localhost:3014/client-metadata.json
{ "access_token": "eyJhbGciOiJIUzI1NiIs…", "token_type": "bearer", "expires_in": 86400, "refresh_token": "…" }

That token works against /mcp like any other:

bunx @modelcontextprotocol/inspector --cli http://localhost:3014/mcp \
  --transport http --header "Authorization: Bearer <access_token>" --method tools/list

Redemption used the document snapshot taken at /authorize, not a second fetch — so the grant is pinned to exactly the metadata the user saw.

C. Rejections worth seeing

Every one of these is an HTTP 400 with a message naming the reason ("if the authorization server fails to retrieve the client metadata document, it SHOULD abort the authorization request"). Run with MCP_CIMD=1 and vary client_id:

Q='response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3014%2Fdemo-callback&code_challenge=WMlOi-4SP-ouuMs3uOb7jhkkMKK5SSJ9Z8Eagvs1wmk&code_challenge_method=S256&state=s'

# nothing hosted there
curl -s "http://localhost:3014/auth/authorize?$Q&client_id=http%3A%2F%2Flocalhost%3A3014%2Fnope.json"
# → client metadata document request returned HTTP 404

# no path component
curl -s "http://localhost:3014/auth/authorize?$Q&client_id=http%3A%2F%2Flocalhost%3A3014"
# → client_id must contain a path component, e.g. https://example.com/client.json

# dot segments
curl -s "http://localhost:3014/auth/authorize?$Q&client_id=http%3A%2F%2Flocalhost%3A3014%2Fa%2F..%2Fclient-metadata.json"
# → client_id must not contain single-dot or double-dot path segments

# a URI the document does not declare
curl -s "http://localhost:3014/auth/authorize?response_type=code&client_id=http%3A%2F%2Flocalhost%3A3014%2Fclient-metadata.json&redirect_uri=http%3A%2F%2Fevil.example%2Fcb&code_challenge=x&code_challenge_method=S256"
# → Invalid redirect_uri

The SSRF refusals (private/loopback addresses, non-https schemes) are not visible in this demo, because allowInsecureClientIdScheme: true is exactly the switch that disables them. Drop MCP_CIMD=1's hatch (edit main.ts) and http://… and https://127.0.0.1/… are both refused before any connection is attempted. tests/mcp-oauth-cimd.e2e.spec.ts covers that posture.

D. Everything at once

PORT=3014 MCP_FAKE_IDP=1 MCP_CONSENT=1 MCP_CIMD=1 npm start

Running against the workspace build? When this example is installed with a file: dependency (scripts/use-examples.sh local) the symlinked packages resolve a second @nestjs/core from the workspace root, which Nest's injector rejects. Add NODE_OPTIONS=--preserve-symlinks — this is a linking artifact, not a product issue, and is what e2e/harness.ts does. A normal npm install of the published packages needs nothing.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选