Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Proxy OAuth Server Provider #159

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

allenzhou101
Copy link

@allenzhou101 allenzhou101 commented Feb 25, 2025

Introduces a new ProxyOAuthServerProvider class that enables proxying OAuth operations to an upstream OAuth server, allowing delegation of OAuth flows while maintaining our interface contract.

Motivation and Context

In many deployment scenarios, we need to integrate with existing OAuth infrastructure (like corporate identity providers or third-party auth services) rather than implementing OAuth flows directly. This proxy implementation provides a clean abstraction layer that allows applications to delegate OAuth operations to an upstream server while maintaining a consistent interface to plug into other parts of the MCP sdk (eg. routing).

How Has This Been Tested?

Using Descope as the external/upstream OAuth IdP and MCP Inspector as the client, the below cases were tested:

  • OAuth endpoint proxying only
  • Combination of OAuth endpoint and overriding (eg. only proxying the token endpoint and explicitly defining authorize)

Breaking Changes

None

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Considerations

  • abstract class instead of class for ProxyOAuthServerProvider, but the latter is slightly easier dx-wise
  • separate mcp auth router function for proxying but probably better to maintain consistent proxy functionality like rate limits and leverage overlap
  • could implement token verification via JWKs from metadata and and client getting via /register out of the box later on
  • also open to different naming like passthrough proxy instead

Usage

const app = express();

const proxyProvider = new ProxyOAuthServerProvider({
    endpoints: {
        authorizationUrl: "https://auth.external.com/oauth2/v1/authorize",
        tokenUrl: "https://auth.external.com/oauth2/v1/token",
        revocationUrl: "https://auth.external.com/oauth2/v1/revoke",
    },
    verifyAccessToken: async (token) => {
        return {
            token,
            clientId: "123",
            scopes: ["openid", "email", "profile"],
        }
    },
    getClient: async (client_id) => {
        return {
            client_id,
            redirect_uris: ["http://localhost:3000/callback"],
        }
    }
})

app.use(mcpAuthRouter({
    provider: proxyProvider,
    issuerUrl: new URL("http://auth.external.com"),
    baseUrl: new URL("http://mcp.example.com"),
    serviceDocumentationUrl: new URL("https://docs.example.com/"),
}))

We could also totally remove the getClient required parameter from the ProxyOAuthServerProvider and add a boolean to the OAuthServerProvider to skip local redirect uri validation and whatever the getClient function is used for to rely on the upstream server. This would simplify implementation while allowing devs to easily override and define to handle validation logic in the server if desired.

@allenzhou101 allenzhou101 marked this pull request as draft February 25, 2025 01:28
@allenzhou101 allenzhou101 marked this pull request as ready for review February 25, 2025 04:18
@allenzhou101 allenzhou101 force-pushed the server-authorization-proxy branch from da8bb7e to 8d7b387 Compare February 27, 2025 21:35
@localden
Copy link

@allenzhou101 was testing this with Entra ID. Thank you for putting this together!

It mostly works with vanilla endpoints, but I feel like there are some gaps. Let me know what the best way you want to collaborate on this (happy to contribute to the PR).

  1. Not every IdP supports Dynamic Client Registration (DCR). This can be a problem and we need to have some kind of vanilla way of providing a default client ID (e.g., in scenarios with public clients this can be hardcoded).
  2. On DCR, maybe this is even something that can be done dynamically by the server itself instead of the IdP.
  3. The code needs to support scopes, and the request to /token needs to also include a redirect URL.
  4. I am not entirely sure why you need both issuer and base URL defined in the router initialization, while also having the IdP URLs constructed in the provider. For a simpler DX, those should probably be consolidated.

For Entra ID specifically, this would kind of work with the public client flow, but is a bit more problematic because the server is responsible for getting the token rather than the client, which means that things like integration with authentication brokers (e.g., WAM) is not possible. This probably is more of a client conversation anyway, but thought I'd call it out here as well.

@allenzhou101
Copy link
Author

allenzhou101 commented Mar 17, 2025

@localden Thanks for your review! I’d definitely appreciate any contributions.

  1. That makes sense. Maybe a defaultClient parameter in ProxyOptions? Although I do wonder if it's a security best practice to bake this in as a default, as you wouldn't be able to validate any redirect URLs.
  2. Certainly! In our Descope implementation we actually implement a subclass of the ProxyOAuthServerProvider and override the register function to use our management api endpoints to handle DCR.
  3. Could you clarify where you’re seeing missing scopes? Based on the OAuth 2.1 specification, the two supported grant types (Authorization Code and Refresh) only require the scope parameter in the authorize and refresh requests, both of which are included. I do agree on the redirect URI!
  4. The purpose of the issuer and base URL distinction is to support cases where the token’s user differs from the base URL (i.e., the MCP Server URL). For example, with external providers, the issuer is likely the external provider’s URL or an auth-related subdomain, whereas the server itself would expose auth proxy endpoints under its own domain.

Would love to collaborate if you’re interested in contributing a PR here.

@csmoakpax8
Copy link

Love this! Basically what I need!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants