# How OAuth Discovery and Authorization Binding Works for MCP Connections in OpenWork

> Learn how OpenWork's OAuth discovery and authorization binding secures MCP connections. Prevent mix-up attacks by validating issuer and authorization server for secure communication.

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

---

**OpenWork implements a two-step OAuth discovery and authorization binding process that validates the MCP server's advertised issuer against the configured authorization server before persisting credentials, preventing mix-up attacks by ensuring connections only communicate with their intended OAuth provider.**

OpenWork's enterprise MCP (Model-Context-Protocol) client manages secure connections to external MCP servers through a rigorous OAuth 2.0 discovery and binding mechanism. This system ensures that when users connect to protected resources, the authorization server used for token issuance is cryptographically bound to the metadata advertised by that resource. Understanding how OAuth discovery and authorization binding works for MCP connections in OpenWork is essential for administrators configuring enterprise deployments and developers extending the platform.

## OAuth Discovery Phase

When a user initiates an MCP connection, OpenWork begins by gathering OAuth metadata from the target server. This discovery phase retrieves the authorization server URL and associated metadata required to establish trust.

### The OAuthDiscoveryState Interface

The discovery response populates an `OAuthDiscoveryState` object containing three critical fields. The `authorizationServerUrl` identifies the OAuth authorization server advertised by the MCP resource. The optional `authorizationServerMetadata` contains RFC 9207 metadata including the issuer claim. Most importantly, `resourceMetadata` includes an optional `authorization_servers` list that may contain resource-scoped discovery aliases—alternative URLs that resolve to the same authorization server.

### Discovery Persistence in EnterpriseMcpOAuthProvider

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) (lines 148-156) persists this discovery state. Before storage, the provider validates that the discovered metadata matches any explicitly configured issuer, ensuring that the connection only proceeds if the advertised server aligns with administrative policy.

## Authorization Server Binding

Binding ensures that an explicitly selected `authorizationServerIssuer` matches the discovery metadata, preventing a compromised resource from redirecting authentication to a malicious provider.

### The isAuthorizationServerDiscoveryBound Function

Located 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) (lines 22-48), the `isAuthorizationServerDiscoveryBound` function implements the core validation logic. This function performs three sequential checks to verify that the issuer selected by the administrator or user is legitimate for the discovered resource.

### Direct Binding and Resource-Scoped Aliases

The binding logic first checks **direct binding** using `isEquivalentOAuthDiscoveryAlias`, which normalizes the scheme and root URL to compare the discovered `authorizationServerUrl` against the expected issuer. It also verifies the issuer appears in the resource-advertised `authorization_servers` list if present.

Second, the function checks for **resource-scoped discovery aliases** via `isResourceScopedDiscoveryAlias`. This handles cases where the resource advertises the same issuer using different URL formats—such as variations with or without trailing slashes—ensuring semantic equivalence without requiring exact string matches.

### Canonical Issuer Verification

Finally, the function performs a **canonical issuer check** against the optional `authorizationServerMetadata`. If metadata is present, the `issuer` field must either match the expected issuer exactly or be undefined. Any mismatch triggers an `MCP_OAUTH_ISSUER_MISMATCH` error, an `EnterpriseMcpOAuthContractError` that halts the connection process.

## Validation During Authorization Response

Binding validation occurs at two critical lifecycle points: during initial discovery persistence and when loading stored discovery state.

### Preventing Mix-Up Attacks

When the OAuth callback is received at the redirect endpoint, OpenWork validates the response issuer against the bound discovery state using `validateMcpAuthorizationResponseIssuer` in [`packages/enterprise-mcp-client/src/authorization-response.ts`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/authorization-response.ts) (lines 33-108). This function ensures the `iss` claim from the authorization response matches the bound issuer discovered earlier. For providers supporting the `authorization_response_iss_parameter`, the implementation also supports distinct-redirect-uri defenses as an additional mix-up protection mechanism.

The `saveDiscoveryState` method invokes `assertDiscoveryBinding` before persisting state, while `discoveryState()` re-runs binding checks when loading credentials. This dual validation protects against configuration drift or compromised persisted data.

## Security and Operational Benefits

This binding architecture provides three primary advantages:

- **Attack Prevention**: Blocks mix-up attacks where a malicious resource attempts to redirect users to an attacker-controlled OAuth provider
- **Credential Scoping**: Ensures tokens, refresh tokens, PKCE verifiers, and client registrations remain strictly scoped to the bound issuer
- **Operational Safety**: Changing the selected issuer automatically invalidates stored discovery state and credentials, forcing a fresh discovery and registration flow that prevents credential leakage across identity providers

## Implementation Example

The following TypeScript demonstrates the complete lifecycle from provider initialization through callback validation:

```typescript
// 1️⃣ Create the provider (used by Den to manage the MCP connection)
import { EnterpriseMcpOAuthProvider } from "@openwork/enterprise-mcp-client"

const provider = new EnterpriseMcpOAuthProvider({
  redirectUri: "https://my.den/api/v1/mcp-connections/oauth/callback",
  connectionId: "conn-123",
  persistence,                 // Your persistence adapters (client, tokens, discovery, …)
  flow: { kind: "connect", authorizationId: "auth-sig-xyz" },
  clientName: "OpenWork Desktop",
  clock,
  lifecycle,
  authorizationTransactionTtlMs: 10 * 60_000,
  expirationSkewMs: 5_000,
  oauthConfiguration: {
    // ← optional: admin-selected issuer
    authorizationServerIssuer: "https://login.example.com",
    requestedScopes: ["offline_access", "profile"],
    applicationType: "web",
  },
})

// 2️⃣ Run discovery – loads and validates binding
const discovery = await provider.discoveryState()
if (!discovery) {
  // No persisted discovery – trigger a fresh fetch from the MCP server
  // (the client code would call the MCP discovery endpoint here)
}

// 3️⃣ Save discovery after a successful fetch
await provider.saveDiscoveryState({
  authorizationServerUrl: "https://login.example.com",
  resourceMetadata: {
    resource: "https://api.example.com",
    authorization_servers: ["https://login.example.com"],
  },
})

// 4️⃣ Authorize – creates the URL the user must visit
const authUrl = new URL("https://login.example.com/authorize")
authUrl.searchParams.set("client_id", "my-client")
authUrl.searchParams.set("redirect_uri", provider.redirectUrl)
authUrl.searchParams.set("response_type", "code")
authUrl.searchParams.set("scope", "offline_access profile")
provider.redirectToAuthorization(authUrl)

// 5️⃣ After callback, validate the response issuer
import { validateMcpAuthorizationResponseIssuer } from "@openwork/enterprise-mcp-client"
const validation = validateMcpAuthorizationResponseIssuer({
  discoveryState: discovery,
  expectedIssuer: provider.authorizationServerIssuer,
  responseIssuer: request.query.iss as string | undefined,
  mixUpDefense: "response-issuer",
})
// → throws if the issuer does not match the bound discovery state

```

## Summary

- **Discovery retrieves metadata**: OpenWork fetches `OAuthDiscoveryState` from the MCP server, capturing the `authorizationServerUrl` and optional resource metadata
- **Binding validates trust**: The `isAuthorizationServerDiscoveryBound` function in [`oauth-discovery-binding.ts`](https://github.com/different-ai/openwork/blob/main/oauth-discovery-binding.ts) verifies that the configured issuer matches the discovered metadata through direct alias comparison and resource-scoped validation
- **Validation occurs at multiple stages**: Binding checks run during `saveDiscoveryState` and `discoveryState()` loading, with additional issuer validation during the OAuth callback via `validateMcpAuthorizationResponseIssuer`
- **Security against mix-up attacks**: The system throws `MCP_OAUTH_ISSUER_MISMATCH` if the authorization response issuer does not match the bound discovery state, preventing credential leakage to unauthorized providers
- **File locations**: Core 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), [`packages/enterprise-mcp-client/src/oauth-provider.ts`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/oauth-provider.ts), and [`packages/enterprise-mcp-client/src/authorization-response.ts`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/authorization-response.ts)

## Frequently Asked Questions

### What happens if the MCP server advertises a different authorization server than configured?

OpenWork throws an `MCP_OAUTH_ISSUER_MISMATCH` error and refuses to persist the discovery state or proceed with authentication. The `isAuthorizationServerDiscoveryBound` function checks that the discovered `authorizationServerUrl` is an equivalent alias of the expected issuer and appears in the resource's `authorization_servers` list, preventing connections to unverified providers.

### How does OpenWork handle URL variations for the same OAuth issuer?

The binding logic uses `isEquivalentOAuthDiscoveryAlias` to normalize scheme and root URL comparisons, while `isResourceScopedDiscoveryAlias` handles semantic equivalents like trailing slash variations. This ensures that `https://login.example.com` and `https://login.example.com/` are treated as the same issuer without requiring exact string matching.

### Where is the OAuth callback validation implemented?

The `validateMcpAuthorizationResponseIssuer` function in [`packages/enterprise-mcp-client/src/authorization-response.ts`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-client/src/authorization-response.ts) validates the OAuth callback. It compares the `iss` parameter from the authorization response against the bound discovery state, supporting both standard issuer validation and the `authorization_response_iss_parameter` defense for distinct redirect URIs.

### Can changing the authorization server issuer invalidate existing MCP connections?

Yes. When the `authorizationServerIssuer` configuration changes, the binding check in `discoveryState()` fails because the new issuer does not match the persisted discovery metadata. This invalidates stored credentials and forces a fresh discovery flow, ensuring tokens remain scoped to the correct provider and preventing cross-issuer credential reuse.