How OpenWork Implements OAuth Flows for Managed MCP Connections in `local-managed-mcp.ts`

local-managed-mcp.ts implements a complete, cryptographically secure OAuth 2.0 lifecycle for Model Context Protocol connections using HMAC-signed state tokens, vault-encrypted credentials, and automatic refresh logic.

The OpenWork repository (different-ai/openwork) provides enterprise-grade Model Context Protocol (MCP) integrations through its local-managed connection handler. This article examines how apps/server/src/local-managed-mcp.ts orchestrates the OAuth 2.0 authorization flow, state verification, and encrypted credential persistence that powers secure MCP server connections.

Starting the Authorization Flow

The OAuth process begins with startLocalManagedMcpAuthorization (lines 992–1015), which initiates the authentication dance and generates a cryptographically secure state token.

First, the function marks the connection status as connecting and writes a provisional runtime entry. It then calls createAuthorizationState to build a JSON payload (AuthorizationStatePayload) containing the workspace ID, connection ID, redirect URI, expiration timestamp, and a random nonce. This payload is base-64 encoded and HMAC-signed using the vault encryption key (vaultKey) via createHmac, producing a tamper-proof state parameter.

The signed state is passed to enterpriseClient().connect along with the redirect URI. If the provider returns an instant connected status, the flow completes immediately. Otherwise, the function returns the provider’s authorizeUrl for browser redirection.

// Initiate OAuth from an API endpoint
const publicConn = await startLocalManagedMcpAuthorization(
  serverConfig,
  workspaceId,
  "my-mcp-connection",
);

// If authorization is required, redirect the user:
// publicConn.authorizeUrl → "https://provider.com/oauth/authorize?..."

Handling the OAuth Callback

After user consent, the provider redirects to the callback URL handled by completeLocalManagedMcpAuthorization (lines 1017–1044). This function validates the returned state and exchanges the authorization code for tokens.

The verifyAuthorizationState helper splits the incoming state string, recomputes the expected HMAC signature, and validates it against the provided signature using timingSafeEqual to prevent timing attacks. It also verifies expiration timestamps and ensures the connection ID matches the stored record.

Once validated, the function retrieves the stored EnterpriseMcpConnection via enterpriseConnection and invokes enterpriseClient().completeAuthorization with the authorization code. After successful token exchange, it runs verifyTools to confirm the tool catalog is accessible, writes the final runtime entry via writeManagedRuntimeEntry, and returns the public connection metadata.

// Callback handler invoked by the OAuth provider
export async function oauthCallback(req: Request) {
  const url = new URL(req.url);
  const state = url.searchParams.get("state")!;
  const code = url.searchParams.get("code")!;

  const { connection, workspaceId } = await completeLocalManagedMcpAuthorization(
    serverConfig,
    state,
    code,
  );

  // connection.status is now "connected" with hasCredential: true
  return Response.json(connection);
}

Token Persistence and Refresh Mechanisms

Credential storage follows a zero-trust encryption model powered by createPersistence, which returns an EnterpriseMcpOAuthPersistence object wired into the enterprise MCP SDK.

All persistence callbacks (load, save, invalidate) operate within encrypted vault transactions (withVaultMutation) using AES-256-GCM encryption. The authorizationStorageKey generates unique HMAC-based keys for each authentication transaction, ensuring isolation between connection instances.

When tokens expire or API requests fail authentication, markReconnectWhenCredentialIsGone detects the missing or invalid credentials and updates the connection status to reconnect_required. This signal triggers the UI to prompt the user for re-authorization. The system automatically handles token refresh via enterpriseClient().refreshCredentials when possible.

// Automatic refresh triggered by the enterprise client
await enterpriseClient().refreshCredentials({
  connection: await enterpriseConnection(serverConfig, workspaceId, "my-mcp-connection"),
  redirectUri: localManagedMcpCallbackUrl(serverConfig),
});

Core Implementation Details

State Generation and CSRF Protection

The createAuthorizationState function constructs a JSON payload containing:

  • workspaceId and connectionId for context binding
  • redirectUri for validation
  • expiresAt timestamp (short-lived)
  • nonce for replay protection

This payload is serialized, base-64 encoded, and signed with the vaultKey using HMAC-SHA256. The final state string follows the format ${encoded}.${signature}, providing cryptographic integrity checks against CSRF and session fixation attacks.

Enterprise MCP Client Architecture

The enterpriseClient() factory creates a configured client via createEnterpriseMcpClient wrapped with guardedFetch. This client handles both the initial connect request and subsequent completeAuthorization calls, enforcing URL validation through local-managed-mcp-url-guard.ts to prevent Server-Side Request Forgery (SSRF) against private networks.

Vault Encryption and Key Rotation

The vault system derives encryption keys from process.env.OPENWORK_ENCRYPTION_KEY or a configuration-provided secret. When the encryption key changes, the vault enters a quarantined state and rebuilds from a non-secret index, forcing credential reconnection to ensure forward secrecy.

Runtime Integration

Authorized connections are persisted to the workspace runtime via runtime-opencode-config-store.ts. The writeManagedRuntimeEntry function inserts a remote MCP configuration pointing to the local gateway (gatewayPath) authenticated with a bearer token (gatewayToken), enabling seamless tool invocation across the OpenWork platform.

Summary

  • startLocalManagedMcpAuthorization initiates OAuth flows with HMAC-signed state tokens to prevent CSRF attacks.
  • completeLocalManagedMcpAuthorization validates callbacks using constant-time signature comparison (timingSafeEqual) before exchanging codes for tokens.
  • All credentials are encrypted with AES-256-GCM and stored in a versioned vault with automatic invalidation on key rotation.
  • The markReconnectWhenCredentialIsGone handler ensures graceful degradation when tokens expire or are revoked.
  • Runtime entries bridge authorized connections into the Opencode execution environment via gatewayPath and gatewayToken authentication.

Frequently Asked Questions

How does OpenWork prevent CSRF attacks in the OAuth flow?

OpenWork binds each authorization request to a cryptographically signed state token generated by createAuthorizationState. This token includes the connection ID, workspace ID, and a nonce, all HMAC-signed with the vault key. During callback processing, verifyAuthorizationState recomputes the signature and uses timingSafeEqual to prevent timing attacks, ensuring the state parameter was not tampered with or replayed from a different session.

What encryption standard protects credentials in local-managed-mcp.ts?

The implementation uses AES-256-GCM encryption for all sensitive data, including client registrations, access tokens, refresh tokens, and PKCE code verifiers. The encryption keys are derived from process.env.OPENWORK_ENCRYPTION_KEY and managed through the vault abstraction layer, ensuring that credentials remain encrypted at rest and are only decrypted within secure mutation contexts.

How does the system handle expired or invalidated OAuth tokens?

When a token expires or an API request returns an authentication error, the markReconnectWhenCredentialIsGone utility detects the missing credential and transitions the connection status to reconnect_required. This status is surfaced to the UI, prompting the user to re-authorize. If the provider supports refresh tokens, enterpriseClient().refreshCredentials automatically attempts to obtain new access tokens before flagging the connection for manual reconnection.

Where is the connection state stored during the authorization process?

Transient authorization state (including the signed state parameter and PKCE verifiers) is stored in the encrypted vault via authorizationStorageKey, keyed by the connection ID. Once authorization completes, persistent connection metadata is written to runtime-opencode-config-store.ts, which maintains the gateway configuration and bearer tokens required for ongoing MCP tool invocations within the workspace runtime.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →