How OAuth Discovery and Authorization Binding Works for MCP Connections in OpenWork
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 (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 (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 (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:
// 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
OAuthDiscoveryStatefrom the MCP server, capturing theauthorizationServerUrland optional resource metadata - Binding validates trust: The
isAuthorizationServerDiscoveryBoundfunction inoauth-discovery-binding.tsverifies 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
saveDiscoveryStateanddiscoveryState()loading, with additional issuer validation during the OAuth callback viavalidateMcpAuthorizationResponseIssuer - Security against mix-up attacks: The system throws
MCP_OAUTH_ISSUER_MISMATCHif 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,packages/enterprise-mcp-client/src/oauth-provider.ts, andpackages/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 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.
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 →