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 identifierlifecycle.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:
- The discovery metadata's issuer matches the MCP-configured
authorizationServerIssuer, or - 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=...:
provider.codeVerifier()retrieves the stored PKCE verifier- The authorization code exchanges for access tokens via the token endpoint
provider.saveTokens(tokens)persists results with automaticexpiresAtcalculation viatokenExpiration()
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →