Skip to content

feat(mcp-server): restrict oauth clients to an allowlist of domains - #1797

Merged
hercemer42 merged 10 commits into
mainfrom
feature/prd-861-restrict-mcp-server-access-to-an-allowlist-of-oauth-clients
Aug 6, 2026
Merged

feat(mcp-server): restrict oauth clients to an allowlist of domains#1797
hercemer42 merged 10 commits into
mainfrom
feature/prd-861-restrict-mcp-server-access-to-an-allowlist-of-oauth-clients

Conversation

@hercemer42

@hercemer42 hercemer42 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an allowedOAuthClients option to the MCP server so administrators can restrict which OAuth client applications may connect. DCR means any client can register and — once a legitimate user signs in — obtain tokens; zero-trust customers asked to cap that to approved clients only (e.g. Dust).

  • agent.mountAiMcpServer({ allowedOAuthClients: ['dust.tt'] }) and FOREST_MCP_ALLOWED_OAUTH_CLIENTS (standalone CLI) — both entry points, mirroring tokenTtl/enabledTools.
  • Enforcement lives in ForestOAuthProvider.clientsStore.getClient(), the single choke point the MCP SDK's authorize and token handlers resolve clients through — covering authorization, code exchange, and refresh with one check and no extra HTTP round trip.
  • A client is allowed only when it has at least one registered redirect URI and every one is on an allowed domain or subdomain (case-insensitive, lookalike-proof). Redirect URIs are matched because they are the one piece of registration metadata an impostor cannot benefit from; self-declared client_name/client_uri are ignored.
  • Disallowed clients get a standard invalid_client (4xx, never a 500, never a redirect to the unvalidated URI) with a targeted error_description that names no allowed domain; the rejection is logged with client id + URIs for support.
  • Omitted option = behavior unchanged. Empty array = startup error. Registration itself is not blocked (it happens on the Forest server); enforcement is use-time only.
  • Native/loopback clients are inherently rejected when the allowlist is set — deliberate, documented as a non-goal in the README.

Testing

  • TDD: 28 new tests across provider (unit), server (supertest through the real express app: exchange+refresh success, authorize 302, 400 rejections on all three grant paths, empty-list construction throw), env parser, and agent pass-through.
  • Scoped runs green: provider+parser 71, server 109, agent 42. eslint/tsc/prettier clean on changed files.

fixes PRD-861

🤖 Generated with Claude Code

Note

Restrict MCP server OAuth clients to an allowlist of domains

  • Adds an allowedOAuthClients option to ForestMCPServer and ForestOAuthProvider that accepts a list of domains; when set, OAuth clients whose redirect URIs do not match an allowed domain (or subdomain) are rejected with a 400 invalid_client error.
  • The allowlist is parsed from the FOREST_MCP_ALLOWED_OAUTH_CLIENTS environment variable (comma-separated) in both the standalone CLI (cli.ts) and the example agent (agent.ts).
  • Domain matching is case-insensitive and covers subdomains; non-http(s) redirect URI schemes are always rejected when the allowlist is active.
  • Risk: constructing ForestMCPServer with an explicitly empty allowedOAuthClients array now throws, preventing silent rejection of all clients.

Changes since #1797 opened

  • Added domain list validation and normalization utility [23eac8b]
  • Integrated domain validation into MCP server initialization [23eac8b]
  • Fixed environment variable parsing in agent initialization [23eac8b]
  • Added server tests for OAuth domain allowlist validation [23eac8b]
  • Documented OAuth domain allowlist validation rules [23eac8b]
  • Extended normalizeDomainList function to reject domain entries containing '@', '?', or '#' characters in addition to the existing rejections for '/', ':', and whitespace, preventing incorrect hostname parsing when these URL delimiter characters are present [ad46ed3]
  • Updated documentation and comments to clarify OAuth client allowlist parsing behavior and loopback redirect URI handling [ad46ed3]
  • Added backslash character to the invalid character validation in normalizeDomainList function within the mcp-server package [6344645]
  • Modified normalizeDomainList function to strip trailing root dots from FQDN-style domain entries and added test coverage [691b6d7]

Macroscope summarized 6ff0797.

Dynamic Client Registration lets any OAuth client obtain tokens once a
legitimate user signs in; zero-trust customers need to cap which client
applications can carry that delegation. Matching uses registered
redirect URI domains because self-declared metadata is spoofable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 3, 2026

Copy link
Copy Markdown

PRD-861

@qltysh

qltysh Bot commented Aug 3, 2026

Copy link
Copy Markdown

1 new issue

Tool Category Rule Count
qlty Structure High total complexity (count = 57) 1

Comment thread packages/mcp-server/src/forest-oauth-provider.ts
@qltysh

qltysh Bot commented Aug 3, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (5)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent/src/agent.ts100.0%
Coverage rating: A Coverage rating: A
packages/mcp-server/src/forest-oauth-provider.ts100.0%
Coverage rating: A Coverage rating: A
packages/mcp-server/src/server.ts100.0%
New Coverage rating: A
packages/mcp-server/src/utils/parse-domain-list.ts100.0%
New Coverage rating: A
packages/mcp-server/src/utils/normalize-domain-list.ts90.9%30
Total97.3%
🤖 Increase coverage with AI coding...
In the `feature/prd-861-restrict-mcp-server-access-to-an-allowlist-of-oauth-clients` branch, add test coverage for this new code:

- `packages/mcp-server/src/utils/normalize-domain-list.ts` -- Line 30

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

A non-http(s) scheme dispatches the callback to whatever local app
registered it, so its hostname says nothing about delivery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hercemer42

Copy link
Copy Markdown
Contributor Author

Claude Fable 5 (claude-fable-5): On the qlty High total complexity (count = 56) structure issue — pushing back on addressing it in this PR. The flagged total is file-level complexity on forest-oauth-provider.ts, which was already the package's largest source file before this change; the PR adds one focused enforcement method (assertClientIsAllowed) and one pure static matcher (isUriOnAllowedDomain), both small and single-responsibility. Reducing the file's total complexity means splitting the provider (token exchange / refresh / verification / client store), which is exactly the opportunistic refactoring our PR conventions exclude from feature PRs. If the team wants that split, it should be its own refactor(mcp-server) PR — happy to file a ticket.

Comment thread packages/mcp-server/src/server.ts Outdated
Comment thread packages/mcp-server/test/utils/parse-domain-list.test.ts
Comment thread packages/mcp-server/test/forest-oauth-provider.test.ts
Comment thread packages/mcp-server/README.md Outdated
Comment thread packages/mcp-server/README.md Outdated
Comment thread packages/mcp-server/README.md Outdated
Comment thread packages/mcp-server/README.md Outdated
Comment thread packages/mcp-server/README.md Outdated
Comment thread packages/mcp-server/README.md Outdated
hercemer42 and others added 2 commits August 4, 2026 11:29
Restore the readme's original formatting so the diff carries only the
new section and env row, shorten the option doc comment, and pin deep
subdomain matching plus verbatim parser pass-through with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/mcp-server/src/forest-oauth-provider.ts
Comment thread packages/mcp-server/src/forest-oauth-provider.ts
hercemer42 and others added 2 commits August 4, 2026 12:08
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/mcp-server/src/cli.ts
Comment thread packages/mcp-server/src/utils/parse-domain-list.ts Outdated
Comment thread packages/mcp-server/src/server.ts Outdated
A set-but-garbage env value parsed to "not configured" and silently
disabled the allowlist, while malformed entries (scheme, port, blanks,
unicode) silently denied or allowed the wrong clients. Entries are now
trimmed, punycode-normalized and validated at startup; a configured
value with no usable domains fails the boot instead of failing open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/_example/src/forest/agent.ts
Comment thread packages/mcp-server/README.md Outdated
Comment thread packages/mcp-server/src/utils/normalize-domain-list.ts Outdated
An '@' in an entry made URL parse the lead as userinfo and normalize
to the wrong hostname, authorizing an unintended domain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/mcp-server/src/utils/normalize-domain-list.ts Outdated
WHATWG URL treats a backslash as a path separator, silently truncating
the entry to a broader domain than the administrator supplied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/mcp-server/src/utils/normalize-domain-list.ts
URL.hostname preserves a terminal dot, which no redirect URI hostname
carries, so the entry could never match anything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hercemer42
hercemer42 merged commit ebaeb17 into main Aug 6, 2026
30 of 32 checks passed
@hercemer42
hercemer42 deleted the feature/prd-861-restrict-mcp-server-access-to-an-allowlist-of-oauth-clients branch August 6, 2026 07:46
forest-bot added a commit that referenced this pull request Aug 6, 2026
# @forestadmin/mcp-server [1.21.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.20.3...@forestadmin/mcp-server@1.21.0) (2026-08-06)

### Features

* **mcp-server:** restrict oauth clients to an allowlist of domains ([#1797](#1797)) ([ebaeb17](ebaeb17))
forest-bot added a commit that referenced this pull request Aug 6, 2026
# @forestadmin/agent [1.92.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.91.6...@forestadmin/agent@1.92.0) (2026-08-06)

### Features

* **mcp-server:** restrict oauth clients to an allowlist of domains ([#1797](#1797)) ([ebaeb17](ebaeb17))

### Dependencies

* **@forestadmin/mcp-server:** upgraded to 1.21.0
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.

2 participants