Skip to content

feat: opt-in shared-secret auth on HTTP/SSE transport - #61

Open
Nshuti7 wants to merge 5 commits into
Dokploy:mainfrom
Nshuti7:add-mcp-auth-token
Open

feat: opt-in shared-secret auth on HTTP/SSE transport#61
Nshuti7 wants to merge 5 commits into
Dokploy:mainfrom
Nshuti7:add-mcp-auth-token

Conversation

@Nshuti7

@Nshuti7 Nshuti7 commented Jul 6, 2026

Copy link
Copy Markdown

Summary

  • Add MCP_AUTH_TOKEN env var. When set, /mcp, /sse, and /messages require the token as either Authorization: Bearer <token> (RFC 6750-clean, preferred) or ?token=<token> query param.
  • Off by default — unset preserves current behavior. Startup log records auth: "bearer token required" or a warning when unset.
  • /health stays open so container orchestrators can probe.
  • Middleware extracted to src/utils/auth.ts as makeAuthMiddleware(token), exercised by 12 vitest cases.

Why

Exposing the HTTP transport publicly today means anyone who can reach the URL can call every tool against the linked Dokploy instance, including destructive ones. IP-allowlisting at a reverse proxy works, but breaks down the moment the client's egress isn't a stable set of IPs — which is the case for hosted Claude clients (Cowork's "Add custom connector" dialog, Claude Desktop's URL-based custom connectors, and similar). Those UIs also can't attach a custom Authorization header, which is why the query-string form exists alongside the header form.

Same shape as #38 (opt-in security feature via env var, backwards compatible) and #40 (opt-in HTTP-transport enabler for hosted deployments).

RFC 6750 note on the query-string form

RFC 6750 discourages tokens in URLs because they can leak via server access logs, referer headers, or browser history. None of those apply here: the endpoint is only ever POST'd by an MCP client (never navigated in a browser), and the server does not log request URLs. The header form remains preferred; the query form exists specifically for URL-only client UIs where it's the only path to any auth at all. Documented in the code and README.

Test cases covered

  • Token unset — pass-through, arbitrary Authorization header ignored
  • Token empty string — treated as unset (no accept-anything trap)
  • No credentials → 401 with WWW-Authenticate: Bearer realm="dokploy-mcp" and a JSON-RPC -32001 Unauthorized body
  • Correct Authorization: Bearer → 200
  • Wrong-value Authorization: Bearer → 401
  • Non-Bearer scheme (Basic) → 401
  • Correct ?token= → 200
  • Wrong-value ?token= → 401
  • Wrong-length token → 401 (guards the timing-safe compare)
  • Header and query both valid → 200
  • Header valid, query garbage → 200 (no fall-through rejection)

Verification

  • pnpm test — 37/37 pass (12 new for auth middleware)
  • pnpm type-check — clean
  • pnpm lint — clean (1 pre-existing warning unrelated to this change)
  • pnpm build — succeeds; build/utils/auth.js produced
  • Manual smoke test against the built binary: /health open (200), no/bad token → 401 on /mcp, valid header → 200, valid ?token= → 200

@Siumauricio @andradehenrique — would appreciate a look when time allows. Backwards-compatible opt-in and I'd love to stop maintaining a fork for this.

Nshuti7 added 5 commits July 6, 2026 22:19
Add MCP_AUTH_TOKEN as an optional env var. When set, every request to
/mcp, /sse, and /messages must present the token as either
'Authorization: Bearer <token>' (RFC 6750-clean, preferred) or
'?token=<token>' query param. The query-string path exists so this
server works with MCP client UIs that only accept a URL — the Cowork
'Add custom connector' dialog and equivalents — which have no way to
attach a custom header.

Comparison is timing-safe. When MCP_AUTH_TOKEN is unset, behavior is
unchanged (endpoints open) and a startup warning is logged. /health
stays open regardless so orchestrators can probe.
Move the shared-secret gate into src/utils/auth.ts as a
makeAuthMiddleware(token) factory so it can be exercised directly with
different token values, without module-load env-var gymnastics.

Add 12 vitest cases covering: unset/empty token (no-op), header-only
accept/reject, query-only accept/reject, mismatched Authorization
scheme, wrong-length token (guards the timing-safe compare), and
header-and-query both present.
Without this, setting MCP_AUTH_TOKEN in the shell (or in Dokploy's
environment editor) wouldn't reach the running process and the gate
would be silently disabled.
Pre-existing upstream issue (Dokploy#59, PR Dokploy#60): the Dockerfile
COPYs pnpm-lock.yaml but .dockerignore excludes it, so builds fail
from a fresh clone. Also pin pnpm to 10.33.0 to avoid 'latest' drift
between build stages, and include .npmrc so registry/config is
consistent with the local install.

Mirrors the upstream fix from Dokploy#60.
Publishing 3000:3000 on the host conflicts with Dokploy's own UI (which
binds :3000) and adds nothing when a reverse proxy is in front — the
proxy reaches the container over the shared Docker network on its
internal port. Left commented for anyone running this compose
standalone without a proxy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant