Skip to content

Toolcraft: add opt-in hosted OAuth configuration and durable store adapters #524

Description

@kamilio

Problem

Toolcraft exposes the low-level OAuth authorization-server primitives added in #519, but an app author still has to assemble production hosted MCP OAuth by hand. That assembly is security-sensitive and has now produced the same class of failures in multiple connectors.

The current primitives make it easy to accidentally ship:

  • process-local dynamic client registration, authorization transactions, codes, grants, refresh-token families, and revocation state;
  • a new signing key on every deploy, invalidating existing clients and tokens;
  • public metadata derived from an internal HTTP socket behind Fly or another TLS proxy;
  • empty access-token scopes when a client omits scope;
  • a successful cross-origin callback blocked by form-action 'self' across the form redirect chain;
  • an authenticated MCP subject that is never bound to the provider account used by tool handlers;
  • process-local MCP transport sessions when ordinary JSON tool calls can be stateless;
  • provider refresh-token races across multiple instances.

This is not connector business logic. Toolcraft should provide a production-shaped hosted OAuth configuration for its existing HTTP server that is difficult to configure incorrectly.

Proposed capability

Extend the existing HTTP server API while retaining the existing low-level escape hatches:

const server = await createHTTPMCPServer(root, {
  name: "calendar",
  version: "1.0.0",
  oauth: hostedOAuth({
    publicUrl: "https://calendar.example/mcp",
    storage,
    provider,
  }),
});

Toolcraft should own discovery, OAuth routes, callback completion, subject propagation, production validation, and stateless protected-resource transport. The connector should implement only provider-specific hooks such as signing in, deriving an opaque stable subject, and opening a subject-scoped provider service.

This capability must be explicitly opt-in. Existing non-OAuth Toolcraft users must not need a database, signing key, encryption key, provider hooks, or any new configuration. Stdio MCP, local/insecure HTTP, pre-shared bearer-token HTTP, and externally managed OAuth remain supported independently.

The common API must also stay small: the existing server constructor, one opt-in hostedOAuth() helper, safe production defaults, and a focused provider hook. Protocol-level controls and custom storage behavior remain progressive-disclosure escape hatches rather than required top-level configuration.

Required pieces

Hosted OAuth configuration

  • Serve both root and path-qualified RFC 9728 protected-resource metadata.
  • Serve authorization-server metadata, JWKS, DCR, authorize, token, and revoke routes.
  • Require a canonical public URL; never infer outward URLs from the backend socket when proxying.
  • Trust forwarded scheme/host only through the existing explicit trusted-proxy policy.
  • Default an omitted authorization scope to configured defaults; never silently mint an empty-scope token.
  • Publish the same supported scopes in both metadata documents and challenges.
  • Complete authorization with a real 303 to the exact registered callback, preserving code, state, and iss.
  • Generate interaction CSP from the validated callback origin so Chromium permits the cross-origin form redirect chain without broadening form-action unnecessarily.
  • Make CSRF cookies safe for concurrent authorization tabs (transaction-specific cookie name or equivalent binding).
  • Set sessionIdGenerator: undefined by default when no stream/subscription capability requires stateful MCP sessions.

Durable authorization-server storage

  • Define a single HostedOAuthStorage contract covering OAuth records, encrypted provider sessions, signing-key persistence, atomic refresh coordination, revocation, and cleanup.
  • Publish conformance-tested Redis and SQLite reference implementations separately from Toolcraft, or keep them application-owned; neither is imported, bundled, or installed by Toolcraft.
  • Keep AuthorizationServerStore public so applications can provide Postgres or another transactional backend.
  • Provide conformance tests that every adapter must pass.
  • Keep concrete storage clients outside the Toolcraft package so core and non-OAuth applications do not install Redis, SQLite, or Postgres dependencies.

Keys and production readiness

  • Load stable signing keys from deployment secrets/KMS and publish current plus previous public JWKs during rotation.
  • Fail production startup for ephemeral stores, ephemeral signing keys, non-HTTPS public URLs, inconsistent scope sets, or missing provider-session encryption.
  • Expose an assertProductionReady() diagnostic with actionable messages.

Subject-bound provider services

  • Pass the verified (issuer, subject, clientId, scopes, resource) into a request-scoped service factory.
  • Fail closed when no provider account is linked; never fall back to a process-global credential.
  • Define a generic encrypted provider-session repository hook with version/CAS and distributed refresh coordination. Passwords are never persisted.
  • Make grant revocation observable so a connector can unlink provider credentials when the final active grant is revoked.

Acceptance criteria

  • A reference connector needs no custom OAuth routing, discovery documents, callback HTML/CSP, proxy URL rewriting, DCR registry, token store, or signing-key lifecycle code.
  • The reference connector's happy path uses the existing createHTTPMCPServer() constructor with oauth: hostedOAuth({ storage, provider }); it does not repeat OAuth endpoint or token-lifecycle configuration.
  • A non-OAuth connector compiles and runs unchanged, with no hosted-OAuth transitive runtime dependencies or startup checks.
  • Importing or calling the ordinary Toolcraft HTTP/stdio APIs does not initialize hosted OAuth infrastructure; only supplying the hostedOAuth() configuration does.
  • ChatGPT DCR + authorization-code PKCE works with its current callback URL and with omitted scope.
  • The browser returns automatically to ChatGPT; no localhost copy/paste step is required.
  • Existing registration, access token, and refresh token survive a deploy/restart with the same store and keys.
  • Two accounts authorize concurrently and 100+ interleaved tool calls never cross provider credentials, frame/account caches, or refresh state.
  • Two app instances racing an upstream rotating refresh token perform one refresh and both use the resulting credential.
  • Refresh-token replay revokes its family and grant; revoking one grant does not affect another subject.
  • Metadata and challenges contain the canonical HTTPS resource URL behind a Fly-style proxy.
  • Logs and persisted records contain no plaintext passwords, provider access tokens, or provider refresh tokens.

Relationship to existing work

This issue is the missing production composition configuration and official persistence story above those primitives.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions