feat(server): add bearer token auth for HTTP transport - #390
Merged
Conversation
Anyone with the HTTP server URL could reach the MCP endpoint unauthenticated. Adds an opt-in --auth-token/DBHUB_AUTH_TOKEN flag: a comma-separated list of bearer tokens checked with constant-time comparison on every request except /healthz. Configuring a token is itself the enforcement switch, so there's no separate flag to forget. Deliberately a flat shared-secret allow-list rather than full OAuth 2.1 resource-server machinery (RFC 9728 metadata, DCR, PKCE), matching the pattern used by mcp-remote, Sentry MCP's self-hosted mode, and the community crystaldba/postgres-mcp nginx template. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds optional Bearer token authentication for DBHub’s HTTP transport to prevent unauthenticated access to the MCP endpoint (and other HTTP routes) when an auth token is configured.
Changes:
- Introduces
--auth-token/DBHUB_AUTH_TOKEN(comma-separated tokens) and validation logic using constant-time comparison. - Enforces Bearer auth on the HTTP Express app for all routes except
/healthz, and updates CORS allow-headers to includeAuthorization. - Adds unit tests for token validation and updates CLI documentation to describe the new flag.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/auth-token.ts | Adds Bearer token parsing + constant-time allow-list validation utility. |
| src/utils/tests/auth-token.test.ts | Adds unit tests covering configured/unconfigured auth token scenarios and header variants. |
| src/server.ts | Wires token enforcement middleware into HTTP transport and updates CORS allow-headers + startup logging. |
| src/config/env.ts | Adds resolveAuthTokens() to source token list from CLI/env configuration. |
| docs/config/command-line.mdx | Documents --auth-token usage and updates quick reference + production warning. |
| CLAUDE.md | Documents the new --auth-token flag in the command-line options list. |
Drop the transport-gated ternary around resolveAuthTokens() — it's cheap to call unconditionally and was duplicating the function's own default-value literal. Move the /healthz route registration ahead of the auth middleware instead of hardcoding a path exemption inside it, so the middleware stays a plain token gate with no awareness of which routes are public. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The design rationale for the shared-secret approach stands on its own; naming other MCP servers' auth implementations isn't necessary to justify it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Give resolveAuthTokens() its own splitTokenList() instead of reusing the host-oriented splitHostList() — decouples token parsing from any future host normalization (lowercasing, port-stripping) that must never touch case-sensitive tokens. - Fix the --auth-token docs example: /mcp requires a JSON-RPC POST body, so the curl example wasn't copy-pasteable. Switched to /api/sources, a plain GET behind the same auth middleware. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/utils/auth-token.ts:55
- Authorization scheme parsing is case-sensitive and strictly requires exactly "Bearer ". Per HTTP auth conventions the scheme token is case-insensitive, and extra whitespace after the scheme is common (e.g. "bearer " or "Bearer "). As written, those valid/real-world headers will be rejected as malformed.
if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
return {
ok: false,
status: 401,
message: "Missing or malformed Authorization header. Expected: Bearer <token>",
src/utils/tests/auth-token.test.ts:18
- The validator should accept common header variations (scheme case-insensitivity and extra whitespace) to avoid interop issues across different HTTP stacks. Add unit tests for these cases so the behavior is locked in.
it('accepts a matching bearer token', () => {
const result = validateAuthToken('Bearer secret123', ['secret123']);
expect(result).toEqual({ ok: true });
});
it('accepts a token that matches any entry in a multi-token list', () => {
const tokens = ['first-token', 'second-token', 'third-token'];
expect(validateAuthToken('Bearer second-token', tokens)).toEqual({ ok: true });
});
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--auth-token/DBHUB_AUTH_TOKEN(comma-separated list of bearer tokens), checked with constant-time comparison on every HTTP request except/healthz. Missing/invalid tokens get401+WWW-Authenticate: Bearer.--require-authflag to forget, and unset (the default) leaves today's unauthenticated behavior unchanged.Test plan
pnpm test— full suite (1282 tests) passes, no regressions.src/utils/__tests__/auth-token.test.tscovering: no tokens configured, valid/invalid/missing token, wrong scheme, multi-token list, case sensitivity, length-mismatch tokens.--auth-token, confirmedcurlwithout a token gets401+WWW-Authenticate: Bearer, wrong token gets401,/healthzreturns200unauthenticated, correct token reaches the MCP handler and gets a realinitializeresponse.--auth-token, confirmed unauthenticated requests still work (no regression).docs/config/command-line.mdx(new--auth-tokensection, Quick Reference row, updated stale--hostwarning) andCLAUDE.md.🤖 Generated with Claude Code