Skip to content

feat(apps): Vanta connection for their MCP server - #1

Open
marcorivm wants to merge 1 commit into
open-edition/reconciledfrom
feat/vanta-connection
Open

feat(apps): Vanta connection for their MCP server#1
marcorivm wants to merge 1 commit into
open-edition/reconciledfrom
feat/vanta-connection

Conversation

@marcorivm

Copy link
Copy Markdown
Member

Adds a Vanta connection so agents can use Vanta's MCP server through the gateway — connect once in the dashboard, then point any MCP client at https://mcp.vanta.com/mcp with no auth and let the gateway inject credentials.

Stacked on open-edition/reconciled (base), rebased onto its current tip. Review only the single commit here.

Why this needed new OAuth plumbing

Vanta's MCP server authorizes as an MCP resource, not through Vanta's REST API. Its live metadata:

mcp.vanta.com/.well-known/oauth-authorization-server
  authorization_endpoint  https://app.vanta.com/oauth/authorize
  token_endpoint          https://api.vanta.com/oauth/token
  registration_endpoint   https://api.vanta.com/oauth/register
  grant_types             authorization_code, refresh_token     ← no client_credentials
  token_endpoint_auth     ["none"]                              ← public client, no secret
  code_challenge_methods  ["S256"]                              ← PKCE required
  scopes                  mcp-api.all:read, mcp-api.all:write   ← not the REST vanta-api.* namespace

So Vanta can't be a client-credentials import (MongoDB Atlas shape), and it can't use BYOC — there is no secret for an operator to bring, so configurable has nothing to hold. Two generic pieces were added alongside the app:

PKCE (RFC 7636) — an oauth method can set pkce: true. The connect route mints the pair, sends the challenge, and keeps the verifier in an httpOnly cookie scoped to the callback path — deliberately not in the signed state, which travels to the provider.

Dynamic client registration (RFC 7591) — a dynamicRegistration app gets a public client minted on demand, cached in the project's AppConfig row keyed by (region, redirectUri), so a moved deployment origin re-registers (a client id is only valid for its registered redirect URI). The callback leg never registers: the authorization code is bound to the client that started the flow, so a fresh client there could only produce a failed exchange. Minting writes config, so it emits an app-config / create audit event.

Gateway

vanta is registered over the three regional MCP hosts, each gated by credential_host_field: "mcp_host", so a token minted for one region can never be injected into another region's host. api.vanta.com is deliberately not registered — these are MCP-scoped tokens the REST API does not accept.

Refresh goes through the existing refresh_token path (the one that persists rotated refresh tokens) rather than try_refresh_credentials, which cannot return a rotated token. That needed a new ClientCredentialMethod::PublicClient (client_id in body, no secret anywhere) plus credential-sourced client id and token endpoint, since for a registered client both are per-connection rather than per-deployment. RefreshConfig gained two optional fields — EE builds defining their own RefreshConfig literals will need them added.

Verified

Against Vanta's live endpoints, without a Vanta account:

  • Dynamic registration accepts the exact body this code sends and returns a public client_id with no secret.
  • The authorize URL built from that real client id returns Vanta's login page — no parameter rejection (PKCE S256, mcp-api.* scopes, RFC 8707 resource all accepted).
  • The token endpoint returns a structured OAuth error for that client versus a bare 401 for an unknown one, confirming the client and request shape are recognized.

Locally: pnpm check clean; API and gateway suites pass (20 new tests covering region→endpoint mapping, credential shape, PKCE enforcement, registration caching/re-registration, and the never-register-on-callback rule).

Not verified: the consent leg, the first real token exchange, and refresh — these need a Vanta Admin login. Worth exercising before merge.

Known limits (deliberate)

  • Regions: US by default. eu/aus resolve end to end from ?region= on the connect URL, but there's no picker in the dashboard yet, so an EU/AUS tenant can't self-serve.
  • No app-permissions catalog entry: every MCP call is POST /mcp, so the host/path/method tool model cannot separate reads from writes — an entry would author misleading "all reads" rules. Policy targets Vanta as a network host (mcp.vanta.com) for now; per-tool rules would need body conditions, whose matching is EE-only.
  • The EE org-scoped connect seam (tryHandleOrgAuthorize) runs before this plumbing, so org-scoped Vanta connections aren't supported yet.
  • Connecting requires a Vanta Admin — Vanta does not allow non-admins to authorize MCP access.

Note

Verification created one anonymous, un-consented client registration at Vanta (vci_a69fa253…, redirect URI https://api.onecli.sh/v1/apps/vanta/callback). Vanta returned no registration access token, so RFC 7592 self-deletion isn't available — only Vanta support can remove it. It grants nothing until an admin authorizes it.

Vanta's MCP server authorizes as an MCP resource, not through the REST
API: its metadata advertises only authorization_code + refresh_token with
token_endpoint_auth_methods_supported: ["none"], S256 PKCE, a dynamic
registration endpoint, and its own scope namespace (mcp-api.*). There is
no client-credentials shortcut and no secret an operator could configure,
so BYOC (`configurable`) has nothing to hold — hence two new pieces of
generic OAuth plumbing alongside the app itself.

- PKCE (RFC 7636): oauth methods can set `pkce: true`; the connect route
  mints the pair, sends the challenge, and keeps the verifier in an
  httpOnly cookie scoped to the callback path — never in the state that
  travels to the provider.
- Dynamic client registration (RFC 7591): `dynamicRegistration` apps get
  a public client minted on demand, cached in the project's AppConfig row
  keyed by (region, redirectUri) so a moved deployment origin
  re-registers. The callback never registers: the code is bound to the
  client that started the flow. Minting is audited as an app-config write.
- Gateway: `vanta` over the three regional MCP hosts, each gated by
  `credential_host_field: "mcp_host"` so a token minted for one region
  can never be injected into another's host. api.vanta.com is
  deliberately absent — these tokens are MCP-scoped. Refresh runs through
  the existing refresh_token path (which persists rotated tokens) via a
  new PublicClient auth mode plus credential-sourced client id and token
  endpoint, since both are per-connection for a registered client.

Regions: US by default; eu/aus resolve end to end from `?region=` on the
connect URL, with no picker in the dashboard yet.

No app-permissions catalog entry: every MCP call is POST /mcp, so the
host/path/method tool model cannot separate reads from writes and an
entry would author misleading "all reads" rules. Policy targets Vanta as
a network host for now.

Verified live: registration is accepted and returns a public client id;
the authorize URL built from it is accepted with no parameter rejection;
the token endpoint recognizes the client. The consent leg and first token
exchange need a Vanta Admin login and are untested.
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