Add MultiAuth for composing multiple token verification sources#3335
Add MultiAuth for composing multiple token verification sources#3335
Conversation
Test Failure Analysis(This comment was updated with a new analysis for workflow run 22525457770.) Summary: A single SSE timeout test fails on Python 3.10/ubuntu-latest due to a timing-sensitive race condition during session initialization — this is unrelated to the PR's changes (which only touch auth code). Root Cause: This test passes on Python 3.13/ubuntu and Python 3.10/windows in this same run, and consistently passes on Suggested Solution: This is a pre-existing flaky test. Two options:
Detailed AnalysisFailing test: Only failing job: Error: Stack trace: Timeout flow: Confirmation it's a flaky/pre-existing issue: The same test suite passes on Related Files
Analysis by Marvin (Claude Code) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9e8a1f41d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| def get_routes(self, mcp_path: str | None = None) -> list[Route]: | ||
| """Delegate route creation to the server.""" | ||
| if self.server is not None: | ||
| return self.server.get_routes(mcp_path) |
There was a problem hiding this comment.
Delegate well-known route generation to wrapped server
MultiAuth only delegates get_routes, so get_well_known_routes() falls back to AuthProvider’s generic filter logic instead of the wrapped server’s specialized implementation. For OAuth-backed servers this drops behavior from OAuthProvider.get_well_known_routes (RFC 8414 path-aware discovery), so a path-based issuer like https://api.example.com/api can expose /.well-known/oauth-authorization-server instead of /.well-known/oauth-authorization-server/api when callers mount auth.get_well_known_routes(...) at root. That breaks discovery in mounted deployments using MultiAuth with OAuthProxy/OAuthProvider.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58902fc193
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| effective_scopes = required_scopes or ( | ||
| server.required_scopes if server else None |
There was a problem hiding this comment.
Honor explicit empty required_scopes override
MultiAuth computes effective_scopes with required_scopes or (...), so passing required_scopes=[] is treated as falsey and silently falls back to server.required_scopes. That makes it impossible to clear inherited scope requirements, even though the constructor documents required_scopes as an override. Because HTTP auth enforcement reads auth.required_scopes from the wrapper, callers can still get scope failures after explicitly requesting no required scopes.
Useful? React with 👍 / 👎.
| def get_routes(self, mcp_path: str | None = None) -> list[Route]: | ||
| """Delegate route creation to the server.""" | ||
| if self.server is not None: | ||
| return self.server.get_routes(mcp_path) |
There was a problem hiding this comment.
Keep delegated auth routes aligned with MultiAuth base_url
When base_url is overridden on MultiAuth, route generation is still delegated directly to self.server.get_routes(mcp_path), which uses the wrapped server’s own base_url to build OAuth/protected-resource metadata. At the same time, the HTTP layer uses the wrapper’s base_url for resource metadata headers, so differing values produce inconsistent discovery URLs in proxied/mounted deployments. If base_url is intended to override the server default, this delegation path leaves discovery metadata out of sync.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e084029a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| def get_routes(self, mcp_path: str | None = None) -> list[Route]: | ||
| """Delegate route creation to the server.""" | ||
| if self.server is not None: | ||
| return self.server.get_routes(mcp_path) |
There was a problem hiding this comment.
Propagate required_scopes override to delegated metadata routes
MultiAuth accepts a required_scopes override, and the HTTP layer enforces auth.required_scopes, but route generation is delegated directly to the wrapped server here. That means OAuth/protected-resource metadata still comes from the wrapped provider’s own scope settings, so an override like required_scopes=[] can remove runtime scope enforcement while still advertising inherited scopes in /.well-known responses. In deployments that rely on discovery metadata, clients will request the wrong scopes or treat the resource as stricter than it actually is.
Useful? React with 👍 / 👎.
…verage - verify_token now catches exceptions from individual sources and continues to the next, so one broken verifier can't take down the whole chain - set_mcp_path propagates to verifiers, not just the server - Fix jwks_url→jwks_uri typo in class docstring - Add tests for raising verifiers, valid-token HTTP acceptance, and set_mcp_path propagation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff6fabb6d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| required_scopes | ||
| if required_scopes is not None | ||
| else (server.required_scopes if server else None) | ||
| ) |
There was a problem hiding this comment.
Propagate scope override to wrapped auth sources
MultiAuth computes an overridden required_scopes value for itself, but the wrapped server/verifiers keep their original scope requirements. Because verify_token delegates to each source's own verifier logic, built-ins like RemoteAuthProvider + JWTVerifier can still reject tokens for missing inherited scopes even when MultiAuth(required_scopes=[]) is set, which produces unexpected 401s and makes the documented override ineffective for those sources.
Useful? React with 👍 / 👎.
|
gtg |
|
It would be nice to provide a mechanism to see which Something that's missing for my use case as well is the ability to have simultaneous authentication of the agent that is forwarding the request, as well as the end-user. For example:
with this MultiAuth, I would have an MCP middleware, but then I would have to compare what I would expect to see in a JWT for the agent, with what I would expect to see in a JWT for an end-user, and deny tool calls based on that. and also, I would only be able to authenticate either the agent or the end-user. I have a hand-crafted application-code solution that solves the simultaneous auth of agent and end-user by separating the tokens into different headers, I also have an upstream WIP that attempts to do the same: https://github.com/PrefectHQ/fastmcp/compare/main...joar:fastmcp:multi-auth?expand=1 The requirement to list tools before end-user is authenticated is a common source of friction between agent frameworks (ADK, langchain) and the MCP standard, adding a separate |
|
@joar thanks for the detailed writeup! I took a look at your branch — the That said, I think this is fighting against the MCP spec in a way that's hard to support at a framework level. MCP doesn't distinguish between list and execute authorization, and maintaining that split (where a client can discover tools it can't call) is something we actively try to avoid in FastMCP. Your I think MultiAuth (multiple verifiers for a single identity) and what you're building (multiple simultaneous identities with different authorization policies) are different enough that they don't belong in the same abstraction. For now I'd suggest keeping your multi-header approach as application-level customization — the extension points are there for exactly this kind of thing. Re: knowing which verifier authenticated the user — happy to consider that separately. |
Production servers often need to accept tokens from multiple authentication sources — an OAuth proxy for interactive MCP clients alongside JWT verification for machine-to-machine tokens, for example. Today there's no built-in way to compose these; you'd have to subclass
AuthProviderand wire the chain yourself.MultiAuthmakes this a one-liner.MultiAuthwraps an optional auth server (the "primary" that owns routes and OAuth metadata) with additionalTokenVerifierinstances. On each request it tries the server first, then each verifier in order, accepting the first successful result. Routes and middleware delegate to the server; verifiers contribute onlyverify_token.🤖 Generated with Claude Code
https://claude.ai/code/session_01WwKYDCqjM2FqYwY5ZNVvjb
Closes #3035
Closes #2499
Closes #1802