Skip to content

Connector OAuth: persistent attempts, completed only by the user who started - #832

Merged
keysersoft merged 5 commits into
mainfrom
keysersoft/connector-oauth-hardening
Oct 3, 2026
Merged

keysersoft merged 5 commits into
mainfrom
keysersoft/connector-oauth-hardening

Conversation

@keysersoft

Copy link
Copy Markdown
Contributor

Second step of the connector setup plan, and a prerequisite for the guided install: the "Authorize with Provider" flow becomes durable and tied to the person who started it.

Pending authorizations are persisted. They lived in an in-memory Map (10 min), so any restart or blue/green deploy between consent and callback lost them, and a second instance would never see them. New table connector_oauth_attempts:

  • state stored as SHA-256;
  • verifier, client id/secret and private_key_jwt settings encrypted with ENCRYPTION_KEY;
  • valid 15 min, single use (takePendingFlow deletes as it reads).

The code is exchanged only for the user who started the flow.

  • GET /api/mcp-oauth/callback no longer exchanges anything. It checks that the state is one we issued and redirects to /connectors/oauth/complete?state&code.
  • That page posts both to POST /api/mcp-oauth/complete (JWT). The server compares the caller with the attempt's userId; anyone else gets 403 and the attempt is spent.
  • Before, the callback exchanged the code for whoever arrived with the state. Someone could start an authorization on their own connector, send the consent link to another person, and get that person's provider tokens.
  • No cookies are involved, so it works the same when frontend and backend sit on different ports (self-hosted).

Smaller pieces

  • Provider errors (access_denied, invalid_scope, invalid_client…) are explained on the complete page, with a link back to the connector.
  • The code is stripped from the address bar before the exchange, and the page sends no referrer.
  • A signed-out visitor is sent to log in and comes back with the same code.
  • authorize accepts an internal returnTo (absolute URLs, //host and backslashes are dropped), for the guided install that comes next.
  • GET /api/connectors/oauth/redirect-uri returns the redirect URI from SERVER_URL. The connector form shows it instead of guessing from window.location (wrong behind a proxy).

Tests

  • Backend: forward vs exchange, other user refused and attempt spent, expired/reused state (410), provider error, returnTo; DB storage with hashed state, encrypted secrets and single use.
  • Frontend e2e: exchange as the signed-in user, server refusal shown and code stripped from the URL, provider error, signed-out redirect.
  • Locally against Postgres 17: all migrations applied, prisma migrate diff reports no drift, and a round trip through the real table works (hashed state, encrypted payload, 15 min TTL, gone after use).
  • Full backend suite passes (6400); typecheck and lint clean.

Deploy note: an authorization started before the deploy and completed after it fails with "expired"; the user starts it again.

…started

- Pending authorizations move from an in-memory map to
  connector_oauth_attempts (state hashed, verifier and client credentials
  encrypted, 15 min, single use), so a restart or blue/green deploy between
  consent and callback no longer loses them.
- The provider callback no longer exchanges the code: it checks the state
  and forwards code + state to /connectors/oauth/complete, which posts them
  to POST /api/mcp-oauth/complete. The code is exchanged only for the user
  who started the flow; anyone else gets 403 and the attempt is spent.
  Before, a consent link started on one person's connector could be
  completed by someone else, handing over that person's tokens.
- Provider errors (access_denied, invalid_scope, invalid_client) are
  explained on the complete page, with a way back to the connector.
- authorize accepts an internal returnTo; GET /api/connectors/oauth/redirect-uri
  returns the redirect URI from SERVER_URL, used by the connector form
  instead of guessing it from the browser's location.
Comment thread packages/backend/src/connectors/mcp-oauth-callback.controller.ts Fixed
Comment thread packages/backend/src/connectors/mcp-oauth-callback.controller.ts Fixed
@keysersoft
keysersoft enabled auto-merge (squash) October 3, 2026 12:05
@keysersoft
keysersoft merged commit 3e936c0 into main Oct 3, 2026
13 checks passed
@keysersoft
keysersoft deleted the keysersoft/connector-oauth-hardening branch October 3, 2026 12:25
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants