Skip to main content
Glama
rinormaloku

built-in-authorization-server

by rinormaloku

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.

Related MCP server: OAuth MCP Server

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 (whoamiHello, Ada Lovelace!), rejects missing / malformed / wrong-secret tokens with 401.

  • disableEndpoints disables a discovery route (→ 404) while keeping the other.

  • @McpUser() projects req.user.


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.

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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A self-hostable OAuth 2.0 server designed for the Model-Context-Protocol (MCP) that enables you to secure your MCP applications with a robust implementation you control.
    1,843 npm
    112
    ISC
  • F
    license
    Not graded
    quality
    D
    maintenance
    An MCP server for OAuth 2.0 authentication supporting Device Code and Client Credentials flows, enabling secure token management for MCP applications.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    A proof-of-concept MCP server implementing OAuth 2.1 authorization with CIMD client registration and PKCE, demonstrating protected resource access and step-up authentication.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Drop-in OAuth 2.1 + Dynamic Client Registration for MCP servers, providing authentication middleware and token verification.
    5 npm
    1
    MIT