How OpenWork Enterprise MCP Client Handles OAuth Authentication with External Services

OpenWork's enterprise MCP client implements a complete OAuth 2.0 authorization flow through the EnterpriseMcpOAuthProvider class, featuring PKCE support, dynamic client registration, strict redirect URI validation, and automatic token refresh.

The OpenWork platform's Managed Connect Platform (MCP) client provides enterprises with a secure, extensible mechanism for authenticating against external OAuth 2.0 providers. This deep dive examines how the open-source implementation orchestrates discovery, registration, authorization, and token management while enforcing strict security policies.

Architecture Overview

The OAuth flow in OpenWork follows a layered architecture. All MCP requests originate from dev/apps/server/src/mcp.ts source, which routes connections to the appropriate MCP capability. When a connection requires OAuth, the framework instantiates an EnterpriseMcpOAuthProvider to manage the complete authorization lifecycle.

The provider integrates with multiple subsystems:

  • Discovery binding — validates authorization server metadata against configured issuers
  • Client registration — supports both static pre-registration and dynamic client registration
  • PKCE generation — secures authorization code exchanges
  • Token persistence — handles storage, expiration checking, and automatic refresh
  • Redirect normalization — ensures consistent browser-based flow handling

Core OAuth Provider Implementation

The EnterpriseMcpOAuthProvider class in packages/enterprise-mcp-client/src/oauth-provider.ts source serves as the central orchestrator.

Key Responsibilities

Function Purpose
discoveryState() Loads cached discovery records or triggers fresh metadata retrieval
assertDiscoveryBinding() Validates discovered issuer matches MCP configuration
clientInformation() Retrieves existing registration or initiates dynamic registration
saveCodeVerifier() / codeVerifier() Manages PKCE verifiers with TTL-based expiration
tokens() Reads credentials, checks expiration, handles refresh logic
saveTokens() Persists token sets while preserving refresh tokens in refresh mode
redirectToAuthorization() Returns authorization URL to the UI layer

Each operation receives a context object created by context() containing:

  • connectionId — unique connection identifier
  • lifecycle.expiresAt — absolute deadline for the MCP operation
  • Abort signal for cancellation

If the lifecycle expires, the provider throws MCP_LIFECYCLE_DEADLINE and terminates the flow.


Authorization Server Discovery and Binding

When an MCP resource advertises multiple authorization servers, OpenWork must verify the correct issuer is selected. This logic resides in packages/enterprise-mcp-client/src/oauth-discovery-binding.ts source.

The isAuthorizationServerDiscoveryBound(state, expectedIssuer) function returns true only when:

  1. The discovery metadata's issuer matches the MCP-configured authorizationServerIssuer, or
  2. A resource-scoped alias matches the expected issuer

Mismatch triggers MCP_OAUTH_ISSUER_MISMATCH, preventing connections to unauthorized endpoints.


Redirect URI Security Policy

OpenWork enforces strict redirect URI validation per RFC 8252 with private-use scheme support. The policy implementation in ee/apps/den-api/src/mcp/oauth-client-policy.ts source includes:

export function isAllowedMcpOAuthRedirectUri(uri: string) {
  // Reject fragments
  if (uri.includes("#")) return false;

  // Private-use scheme for native MCP clients
  if (MCP_OAUTH_PRIVATE_USE_REDIRECT_URIS.has(uri)) return true;

  // HTTPS is always allowed
  if (new URL(uri).protocol === "https:") return true;

  // HTTP allowed only for loopback hostnames
  if (new URL(uri).protocol === "http:") {
    return isLoopbackHostname(new URL(uri).hostname);
  }
  return false;
}

Validation failures produce MCP_OAUTH_REDIRECT_URI_ERROR_DESCRIPTION, blocking potentially malicious configurations.


Normalizing OAuth Redirects

The OpenWork server handles heterogeneous OAuth response formats. In ee/apps/den-api/src/routes/auth/oauth-redirect.ts source, the normalizeOAuthAuthorizeRedirect helper converts JSON-enveloped responses (used by Better Auth) into standard 302 redirects:

export async function normalizeOAuthAuthorizeRedirect(response: Response) {
  const ct = response.headers.get("content-type")?.toLowerCase() ?? "";
  if (!ct.includes("application/json")) return response;
  const payload = await response.clone().json();
  const url = readRedirectUrl(payload);
  if (!url) return response;
  return new Response(null, { status: 302, headers: buildRedirectHeaders(response, url) });
}

This ensures browser-based flows operate consistently regardless of the authorization server's response format.


Complete OAuth Flow Example

The following demonstrates the EnterpriseMcpOAuthProvider API as implemented in production:

import { EnterpriseMcpOAuthProvider } from "@openwork/enterprise-mcp-client/src/oauth-provider.js";

// Initialize persistence adapters and lifecycle context
const persistence = /* EnterpriseMcpOAuthPersistence implementation */;
const clock = { now: () => Date.now() };
const lifecycle = {
  signal: new AbortController().signal,
  expiresAt: Date.now() + 5 * 60_000  // 5 minute deadline
};

const provider = new EnterpriseMcpOAuthProvider({
  redirectUri: "https://my.app/callback",
  connectionId: "conn-123",
  persistence,
  flow: { kind: "connect", authorizationId: "signed-tx-id" },
  clientName: "My OpenWork Integration",
  clock,
  lifecycle,
  authorizationTransactionTtlMs: 10 * 60_000,
  expirationSkewMs: 30_000,
  oauthConfiguration: {
    requestedScopes: ["openid", "profile", "email"],
    applicationType: "web",
  },
});

// Discover authorization server metadata
const discovery = await provider.discoveryState();

// Register client (dynamic or static)
await provider.saveClientInformation({
  client_id: "generated-client-id",
  client_secret: "generated-secret",
  client_secret_expires_at: Math.floor(Date.now() / 1000) + 3600,
});

// Build authorization URL with PKCE
const authUrl = new URL(discovery?.authorizationServerMetadata?.authorization_endpoint ?? "");
authUrl.searchParams.set("client_id", "generated-client-id");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("redirect_uri", provider.redirectUrl);
authUrl.searchParams.set("scope", provider.requestedScopes.join(" "));
authUrl.searchParams.set("code_challenge", "<base64-url-sha256-pkce>");
authUrl.searchParams.set("code_challenge_method", "S256");

// Store verifier and redirect user
await provider.saveCodeVerifier("<base64-url-pkce-verifier>");
provider.redirectToAuthorization(authUrl);

Callback Handling

Upon return to https://my.app/callback?code=...&state=...:

  1. provider.codeVerifier() retrieves the stored PKCE verifier
  2. The authorization code exchanges for access tokens via the token endpoint
  3. provider.saveTokens(tokens) persists results with automatic expiresAt calculation via tokenExpiration()

Key Source Files Reference

File Path Responsibility
dev/apps/server/src/mcp.ts MCP entry point, routing, and configuration
packages/enterprise-mcp-client/src/oauth-provider.ts Core EnterpriseMcpOAuthProvider implementation
packages/enterprise-mcp-client/src/oauth-discovery-binding.ts Issuer validation and authorization server binding
ee/apps/den-api/src/mcp/oauth-client-policy.ts Redirect URI security policy enforcement
ee/apps/den-api/src/routes/auth/oauth-redirect.ts Response format normalization
packages/enterprise-mcp-client/src/oauth-resource-alias.ts Discovery alias equivalence checking

Summary

OpenWork's enterprise MCP client delivers OAuth authentication through:

  • RFC-compliant discovery — cached metadata with issuer binding validation
  • Flexible client registration — static pre-registration or dynamic OIDC registration
  • PKCE-secured authorization — verifier generation, storage, and validation
  • Hardened redirect handling — strict URI policies and response normalization
  • Resilient token management — automatic refresh, expiration skew tolerance, and lifecycle integration

The implementation in packages/enterprise-mcp-client provides enterprises with a production-ready OAuth 2.0 client that balances security rigor with operational flexibility.


Frequently Asked Questions

Does OpenWork support dynamic client registration per OIDC standards?

Yes. The EnterpriseMcpOAuthProvider in oauth-provider.ts implements dynamic registration through clientMetadata generation. When no pre-registered client exists, the provider constructs registration metadata including client_name, redirect_uris, and application_type, then submits to the authorization server's registration endpoint. The resulting client_id and client_secret are persisted for subsequent flows.

How does OpenWork handle OAuth token expiration and refresh?

The tokens() method checks expiration against clock.now() with configurable expirationSkewMs. When an access token expires and a refresh token exists, the provider automatically initiates refresh. The saveTokens() operation preserves existing refresh tokens when operating in refresh mode, preventing credential loss during token rotation.

What security measures protect against authorization server spoofing?

The assertDiscoveryBinding function in oauth-discovery-binding.ts validates that discovered authorization server metadata contains an issuer matching the MCP configuration. The check supports resource-scoped aliases for flexibility, but any issuer mismatch throws MCP_OAUTH_ISSUER_MISMATCH, blocking connections to potentially malicious endpoints.

Can OpenWork integrate with native mobile applications using custom URL schemes?

Yes. The redirect URI policy in oauth-client-policy.ts explicitly supports private-use URI schemes through MCP_OAUTH_PRIVATE_USE_REDIRECT_URIS. Native applications can register schemes like com.myapp://oauth/callback, which bypass HTTPS requirements while maintaining security through scheme exclusivity.

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 →