Skip to content

fix: validate mcp.issuer is a full origin at config load (#139) - #152

Merged
heskew merged 4 commits into
mainfrom
fix/139-validate-mcp-issuer-origin
Jul 1, 2026
Merged

heskew merged 4 commits into
mainfrom
fix/139-validate-mcp-issuer-origin

Conversation

@heskew

@heskew heskew commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Turns the "pin a full origin" documentation convention for mcp.issuer into an enforced invariant. When mcp.issuer is set, updateConfiguration now validates it is an absolute http(s) origin — parseable by new URL(), http: or https: scheme, no path (beyond a trailing slash), no query, no fragment — and throws a clear error otherwise.

The existing presence check (mcp.enabled && !mcp.issuer) is unchanged.

Why origin-only

resolveIssuer returns mcpConfig.issuer verbatim (minus trailing slash). buildAuthorizationServerMetadata prefixes every endpoint URL with it (${issuer}/oauth/mcp/authorize, etc.). protectedResourceMetadataUrl calls new URL(resolveResource(...)). A path-bearing value like https://host/base would silently produce https://host/base/oauth/mcp/authorize — not the correct path shape — and a schemeless value like as.example.com would throw inside new URL() at request time rather than at startup.

Validation logic

let parsedIssuer: URL;
try { parsedIssuer = new URL(mcpConfig.issuer); } catch { throw ... }
if (protocol !== 'http:' && protocol !== 'https:') throw ...;
const stripped = parsedIssuer.pathname.replace(/\/+$/, '');
if (stripped !== '' || parsedIssuer.search !== '' || parsedIssuer.hash !== '') throw ...;

Trailing slash tolerated — resolveIssuer already strips it.

Tests added (test/options-watcher.test.js)

  • https://app.example.com → passes
  • http://localhost:9926 → passes
  • https://app.example.com/ (trailing slash) → passes
  • as.example.com (schemeless) → throws with mcp.issuer must be an absolute http(s) origin
  • https://app.example.com/base (path-bearing) → throws

Pre-existing cases (enabled+no-issuer, resource-only-pinned) unchanged.

Closes #139

🤖 Generated with Claude Code — Claude Sonnet 4.6

The existing check only caught a missing issuer when mcp.enabled. A schemeless
value like `as.example.com` or a path-bearing one like `https://host/base`
would pass the check but produce malformed JWT `iss` claims and broken endpoint
URLs downstream (resolveIssuer returns it verbatim; buildAuthorizationServerMetadata
prefixes every endpoint URL with it; protectedResourceMetadataUrl calls new URL()
on the composed resource URI).

Add a fast-fail validation: when mcp.issuer is present, parse it with new URL(),
assert the protocol is http/https, and assert there is no path beyond a trailing
slash, no query, and no fragment. Trailing slash is tolerated (resolveIssuer strips
it). Error message names the bad value and the requirement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@heskew
heskew requested a review from kriszyp July 1, 2026 16:53

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds validation for the mcpConfig.issuer option to ensure it is a valid absolute http(s) origin without paths, queries, or fragments, along with corresponding unit tests. The reviewer suggested refactoring the validation logic to avoid duplicating the error message across multiple checks.

Comment thread src/index.ts Outdated
@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found. Prior 1 finding resolved by e0c11cd.

heskew and others added 2 commits July 1, 2026 12:06
Addresses Gemini review on #152 — parse once and evaluate all origin
constraints together, throwing one error, instead of three duplicated
messages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The issuer-format check ran whenever mcp.issuer was present, so
enabled: false with a stale/invalid issuer would throw at startup and
leave stale MCP state on live reload — a regression of the enabled
master switch. Gate the format validation on mcpConfig.enabled and add
a regression test (disabled + invalid issuer must start cleanly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread test/options-watcher.test.js
Comment thread src/index.ts
…entials

Gemini review on #152: add explicit tests for query-string and fragment
rejection (previously only schemeless/path were tested), and reject
userinfo (user:pass@host) which is not part of an origin and would leak
into iss / endpoint URLs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@heskew
heskew marked this pull request as ready for review July 1, 2026 18:39
@heskew
heskew merged commit fcc89af into main Jul 1, 2026
14 checks passed
@heskew
heskew deleted the fix/139-validate-mcp-issuer-origin branch July 1, 2026 18:39
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.

Validate mcp.issuer is a full origin (with scheme) at config load

1 participant