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.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rinormaloku/mcp-inspector-v2-bug-authz'

If you have feedback or need assistance with the MCP directory API, please join our Discord server