Skip to content

fix(mcp): enforce auth on /streamable for auth_type=oauth projects#12756

Open
erichare wants to merge 5 commits intorelease-1.10.0from
fix/mcp-oauth-streamable-auth
Open

fix(mcp): enforce auth on /streamable for auth_type=oauth projects#12756
erichare wants to merge 5 commits intorelease-1.10.0from
fix/mcp-oauth-streamable-auth

Conversation

@erichare
Copy link
Copy Markdown
Collaborator

Summary

  • POST /api/v1/mcp/project/{id}/streamable (and /sse) did not enforce authentication for projects configured with auth_type=oauth when MCP Composer was enabled.
  • Unauthenticated requests returned HTTP 200 with full initialize and tools/list responses, exposing project tools without any credentials. auth_type=apikey correctly rejected unauthenticated requests, confirming the enforcement layer existed but did not trigger for OAuth-configured projects.
  • Root cause: verify_project_auth in src/backend/base/langflow/api/v1/mcp_projects.py only gated the API-key branch on auth_settings.auth_type == "apikey"; any other value (including oauth) fell through to a get_user_by_username(SUPERUSER) fallback that returned the superuser without validating any credential.
  • Fix: treat auth_type=oauth the same as auth_type=apikey on the direct langflow MCP transport endpoints — require a valid API key or return HTTP 401. OAuth end users should continue to connect via the configured MCP Composer OAuth endpoint, which proxies to these endpoints with backend credentials. Error message for the OAuth case is explicit so developers hitting the endpoint directly understand the correct path.

Test plan

  • New unit tests in src/backend/tests/unit/api/v1/test_mcp_projects.py:
    • test_streamable_rejects_unauthenticated_oauth_project — 401 on no-auth POST
    • test_streamable_rejects_unauthenticated_oauth_project_trailing_slash — trailing-slash variant also rejected
    • test_sse_rejects_unauthenticated_oauth_project/sse endpoint also rejected
    • test_streamable_oauth_project_accepts_valid_api_key — valid API key still works
    • test_streamable_oauth_project_rejects_invalid_api_key — bogus API key returns 401 "Invalid API key"
  • Full test_mcp_projects.py passes locally (42 tests, 0 failures)
  • Manual reproduction: re-run the curl / MCP Inspector steps from the bug report with and without an API key; confirm AUTO_LOGIN=true and AUTO_LOGIN=false both enforce now
  • Verify MCP Composer still works end-to-end against an OAuth project after the fix (composer needs to be configured to pass an API key to the langflow backend for the forwarded requests; tracked as follow-up if not already in place)

Notes

  • The fix is minimal and localized to verify_project_auth; it does not change the behavior for auth_type=apikey, auth_type=none with AUTO_LOGIN=true, or the non-composer JWT path.
  • If a deployment relied on the previous (insecure) fallthrough where OAuth projects were reachable without credentials, the langflow-to-composer glue may need to be updated to pass an API key — but the previous behavior was a direct bypass of the configured authentication, so this is the intended break.

…rojects

Previously, projects configured with auth_type=oauth did not enforce
authentication on the /streamable (and /sse) endpoints when MCP Composer
was enabled. verify_project_auth only enforced API key authentication for
auth_type=apikey; for oauth it fell through to the superuser fallback,
returning HTTP 200 with full initialize/tools/list payloads to
unauthenticated requests.

This treats auth_type=oauth the same as auth_type=apikey on the direct
langflow MCP transport endpoints: requests must present a valid API key
or they are rejected with HTTP 401. End users of OAuth-configured
projects should continue to connect via the configured MCP Composer
OAuth endpoint (which proxies to these endpoints with backend
credentials).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e261b5d8-6f71-4ae2-99a4-578bcfd168ee

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-oauth-streamable-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the bug Something isn't working label Apr 17, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 20.00000% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.73%. Comparing base (351e258) to head (0e9b064).
⚠️ Report is 1 commits behind head on release-1.10.0.

Files with missing lines Patch % Lines
src/backend/base/langflow/api/v1/mcp_projects.py 20.00% 8 Missing ⚠️

❌ Your patch check has failed because the patch coverage (20.00%) is below the target coverage (40.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (49.93%) is below the target coverage (60.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.10.0   #12756      +/-   ##
==================================================
- Coverage           52.80%   52.73%   -0.08%     
==================================================
  Files                2025     2025              
  Lines              184053   183350     -703     
  Branches            28838    26108    -2730     
==================================================
- Hits                97191    96685     -506     
+ Misses              85767    85571     -196     
+ Partials             1095     1094       -1     
Flag Coverage Δ
backend 55.88% <20.00%> (+<0.01%) ⬆️
frontend 52.67% <ø> (-0.11%) ⬇️
lfx 49.93% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/api/v1/mcp_projects.py 48.77% <20.00%> (-0.06%) ⬇️

... and 126 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 34%
34.74% (39851/114703) 67.39% (5443/8076) 35.73% (932/2608)

Unit Test Results

Tests Skipped Failures Errors Time
3818 0 💤 0 ❌ 0 🔥 7m 50s ⏱️

…ment

Address review feedback on PR #12756. Requiring an API key for every OAuth
request would have broken the intended MCP Composer proxy path: the composer
subprocess is started with only the Langflow endpoint URL and OAuth env vars
and does not currently inject a backend x-api-key when it forwards
authenticated OAuth traffic, so hardening /streamable and /sse would turn
every legitimate composer-forwarded request into a 401.

Keep the enforcement against direct unauthenticated external access, but
trust requests whose direct TCP peer is a loopback address (the on-host
MCP Composer subprocess). Remote callers still need a valid x-api-key to
hit the project transport endpoints when auth_type=oauth.

Add coverage for the loopback passthrough and the _is_loopback_client
helper, and gate the rejection tests on a patched non-loopback client so
they exercise the remote path even though httpx AsyncClient reports
127.0.0.1 by default.
@erichare
Copy link
Copy Markdown
Collaborator Author

Addressing the P1 review on the composer forwarding path in 52ee309.

Context. I looked at mcp-composer --help directly — it does not accept a downstream auth header/key flag today (options are limited to --auth_type, --remote_auth_type, --client_auth_type, --env KEY VALUE, etc.), and nothing in _do_start_project_composer_impl passes a backend credential. So the reviewer is correct: the previous commit would have started returning 401 for every composer-forwarded request.

Change. Keep the hardening against direct unauthenticated external access, but trust requests whose direct TCP peer (request.client.host) is a loopback address. MCP Composer is spawned by Langflow as a local subprocess and connects back over loopback via get_project_streamable_http_url(), so the proxy path keeps working unchanged. Remote callers still have to present a valid x-api-key for auth_type=oauth.

New / updated coverage in test_mcp_projects.py:

  • test_is_loopback_client_detects_ipv4_and_ipv6_loopback — unit-tests the helper for IPv4/IPv6 loopback, non-loopback, empty, and None.
  • test_streamable_allows_loopback_oauth_for_composer_forwarding — asserts the composer-from-loopback path still returns 200 without a key.
  • Rejection tests now go through a force_non_loopback_client fixture that patches _is_loopback_client to False, so they exercise the remote path (httpx's AsyncClient reports 127.0.0.1 by default, which would otherwise hit the loopback passthrough).

Follow-up (separate PR). Longer-term, a project-scoped backend key should be provisioned on composer startup and plumbed through so mcp-composer can inject x-api-key on forwarded requests — at which point the loopback passthrough can be removed. That requires a matching mcp-composer change (no CLI flag exists today), which is why it is out of scope for this fix.

All 44 tests in test_mcp_projects.py pass locally.

@github-actions github-actions bot added bug Something isn't working and removed bug Something isn't working labels Apr 17, 2026
…uth transport

Follow-up to PR #12756. The loopback-based passthrough from the prior
commit is unsafe in deployments where Langflow sits behind a same-host
reverse proxy or sidecar: the proxy becomes the direct TCP peer, so
external unauthenticated traffic satisfies the loopback check and falls
through to the superuser, reopening the original bypass.

There is no composer-specific identity on the wire today — mcp-composer
forwards without any Langflow credential — so loopback address cannot
distinguish the trusted subprocess from another loopback peer. Require
a valid x-api-key on /streamable and /sse for every OAuth project
regardless of source. Until mcp-composer can forward a project-scoped
backend credential, the composer proxy path will return 401; this is
the intended secure behavior.

Remove the _is_loopback_client helper, the client_host plumbing into
verify_project_auth, and the associated tests. Drop the
force_non_loopback_client fixture; the rejection tests now exercise the
real code path directly.
@erichare
Copy link
Copy Markdown
Collaborator Author

Agreed — loopback trust is unsafe in reverse-proxy / sidecar deployments where external traffic comes in from the proxy on 127.0.0.1. Reverted in 76d72c9.

Now: /streamable and /sse require a valid x-api-key for any auth_type=oauth project, regardless of source. No network-level trust, no superuser fallback.

Tradeoff explicitly taken: until mcp-composer can forward a project-scoped backend credential, the OAuth composer proxy path will return 401. I verified earlier that the current mcp-composer CLI has no downstream auth-header flag (--auth_type, --remote_auth_type, --client_auth_type, --env KEY VALUE are the only auth-adjacent options), so the composer-side change is a separate coordinated release. The bypass is closed; the composer regression is the honest cost of closing it.

Tests now exercise the real code path directly — no more force_non_loopback_client patch. 42 tests pass in test_mcp_projects.py.

Yes, please sketch the safer composer-auth handoff design. My current thinking is:

  1. Langflow provisions a project-scoped ApiKey owned by the project's user (name scoped to the composer instance, e.g. MCP Composer internal - {project_name}).
  2. At composer launch, pass the key via --env LANGFLOW_BACKEND_API_KEY <key> and update mcp-composer to read that env var and inject it as x-api-key on every forwarded request.
  3. On auth-settings change / project deletion, rotate or revoke the key.
  4. On the Langflow side, verify_project_auth already accepts x-api-key — no further change needed once mcp-composer forwards it. Would be good to gate on the key belonging to the composer-owned ApiKey (e.g. by tracking the id) to avoid any user API key being usable as a composer backend credential.

Happy to adjust if you see a cleaner shape.

@github-actions github-actions bot added bug Something isn't working and removed bug Something isn't working labels Apr 17, 2026
The previous message asked callers to connect through MCP Composer, but
the composer proxy path currently returns 401 itself (see PR #12756
discussion) because mcp-composer cannot forward a backend credential
yet. Describe the actual working path — use an x-api-key — and note
that composer-based access is pending credential forwarding support.
@github-actions github-actions bot added bug Something isn't working and removed bug Something isn't working labels Apr 17, 2026
Copy link
Copy Markdown
Member

@Cristhianzl Cristhianzl left a comment

Choose a reason for hiding this comment

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

lgtm

@github-actions github-actions bot added lgtm This PR has been approved by a maintainer bug Something isn't working and removed bug Something isn't working labels Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants