# How OpenWork Enterprise MCP Client Handles OAuth Authentication with External Services

> Learn how OpenWork enterprise MCP client uses OAuth 2.0 with PKCE support and dynamic client registration. Securely authenticate with external services and manage token refreshes effortlessly.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-15

---

**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](https://github.com/different-ai/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`](https://github.com/different-ai/openwork/blob/main/dev/apps/server/src/mcp.ts) [source](https://github.com/different-ai/openwork/blob/dev/apps/server/src/mcp.ts), 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`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/oauth-provider.ts) [source](https://github.com/different-ai/openwork/blob/dev/packages/enterprise-mcp-client/src/oauth-provider.ts) 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`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/oauth-discovery-binding.ts) [source](https://github.com/different-ai/openwork/blob/dev/packages/enterprise-mcp-client/src/oauth-discovery-binding.ts).

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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/mcp/oauth-client-policy.ts) [source](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/mcp/oauth-client-policy.ts) includes:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/auth/oauth-redirect.ts) [source](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/routes/auth/oauth-redirect.ts), the `normalizeOAuthAuthorizeRedirect` helper converts JSON-enveloped responses (used by Better Auth) into standard 302 redirects:

```typescript
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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/dev/apps/server/src/mcp.ts) | MCP entry point, routing, and configuration |
| [`packages/enterprise-mcp-client/src/oauth-provider.ts`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/oauth-provider.ts) | Core `EnterpriseMcpOAuthProvider` implementation |
| [`packages/enterprise-mcp-client/src/oauth-discovery-binding.ts`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/auth/oauth-redirect.ts) | Response format normalization |
| [`packages/enterprise-mcp-client/src/oauth-resource-alias.ts`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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.