Front-Door Auth#696
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR implements "Front-Door Authentication" for the Arcade MCP server, adding OAuth 2.1-compliant server-level authentication for HTTP transport. The implementation validates Bearer tokens on every HTTP request before processing MCP protocol messages, enabling secure HTTP transport with tool-level authorization and OAuth discovery support.
Key Changes:
- Added JWT-based token verification with JWKS support for validating Bearer tokens
- Implemented OAuth 2.0 Protected Resource Metadata (RFC 9728) for client discovery
- Added support for multiple authorization servers with flexible configuration via environment variables or code
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| libs/tests/arcade_mcp_server/test_server_auth.py | Comprehensive test suite for JWT verification, RemoteOAuthProvider, middleware, and multi-authorization server scenarios |
| libs/tests/arcade_mcp_server/test_server.py | Added authenticated user context to HTTP transport tests |
| libs/tests/arcade_mcp_server/test_mcp_app.py | Updated tests to include server_auth_provider parameter |
| libs/arcade-mcp-server/pyproject.toml | Added python-jose and httpx dependencies |
| libs/arcade-mcp-server/arcade_mcp_server/worker.py | Integrated auth middleware and OAuth discovery routes |
| libs/arcade-mcp-server/arcade_mcp_server/types.py | Extended SessionMessage to carry authenticated user info |
| libs/arcade-mcp-server/arcade_mcp_server/transports/http_streamable.py | Modified to extract and pass authenticated user from scope |
| libs/arcade-mcp-server/arcade_mcp_server/settings.py | Added ServerAuthSettings for environment-based configuration |
| libs/arcade-mcp-server/arcade_mcp_server/session.py | Updated to handle authenticated user context per request |
| libs/arcade-mcp-server/arcade_mcp_server/server_auth/ | New module with base classes, JWT verifier, RemoteOAuthProvider, and ASGI middleware |
| libs/arcade-mcp-server/arcade_mcp_server/server.py | Enhanced to use authenticated user from front-door auth for tool context |
| libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py | Added auth parameter and removed duplicate .env loading |
| libs/arcade-mcp-server/arcade_mcp_server/fastapi/auth_routes.py | OAuth discovery endpoint implementation |
| libs/arcade-mcp-server/arcade_mcp_server/context.py | Added authenticated_user to Context initialization |
| examples/mcp_servers/authorization/ | Complete example server demonstrating front-door auth with Docker setup |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| - ARCADE_SERVER_TRANSPORT=http | ||
| - ARCADE_SERVER_HOST=0.0.0.0 | ||
| - ARCADE_SERVER_PORT=8001 | ||
| - MCP_RESOURCE_SERVER_CANONICAL_URL=http://127.0.0.1:8080/mcp |
There was a problem hiding this comment.
probably out of scope of this PR, but I've generally seen this as PROXY_URL or something for when the server is behind a proxy of any kind.
| Returns: | ||
| Dictionary containing resource metadata per RFC 9728 | ||
| """ | ||
| return { |
There was a problem hiding this comment.
Assuming this doesn't change, I would construct this at init time so that it can be returned very efficiently.
Then, some other values to consider adding to the document now:
scopes_supported: Should contain the scopes required to access the MCP server itself (maybe justmcp?; not the tool scopes! — seems like we aren't validating scopes at all right now though, so maybe not worth it!bearer_methods_supported: Probably justheader?
And some more to consider in the future:
resource_name: A human readable name to display to the end user; could be settable by some config somewhere?resource_documentation: A URL for docs about this MCP server; config?resource_policy_uri: A URL for a human readable policy document; config?resource_tos_uri: A URL for terms of service; config?
There was a problem hiding this comment.
Thanks! I implemented the following:
- construct resource metadata at init time
- added
bearer_methods_supported
For the others:
- Holding off on scopes entirely for now
- Will add a mention of resource_* to planning document for phase 2
| # ``` | ||
| # resource_server = ResourceServer() | ||
|
|
||
| app = MCPApp(name="authorization", version="1.0.0", log_level="DEBUG", auth=resource_server) |
There was a problem hiding this comment.
very nitty nit: It feels weird to pass a ResourceServer to auth, because what I'm building here (the MCP server) is a resource server. Maybe ResourceServerAuthorization as a class name instead?
There was a problem hiding this comment.
Not nitty at all! I'm going to go with ResourceServerAuth
| that are not compliant with MCP. | ||
|
|
||
| Note: Token signature verification is always enabled and cannot be disabled. | ||
| Additionally, the subject (sub claim) must always be present in the token. |
There was a problem hiding this comment.
We may have to lift the sub claim requirement - we'll see. Some AS don't return it, or return some non-standard claim name. I think it's OK to make this required for now.
| issuer="https://your-workos.authkit.app", | ||
| jwks_uri="https://your-workos.authkit.app/oauth2/jwks", | ||
| validation_options=AccessTokenValidationOptions(verify_aud=False), | ||
| expected_audiences=["your-authkit-client-id"], # Override expected aud claim |
There was a problem hiding this comment.
nit: from what we were discussing yesterday it seems like in the WorkOS case it's an app or workspace ID, not a client ID right?
There was a problem hiding this comment.
So their naming of things is very bad, but it is the AuthKit's Client ID (which is closer to a workspace ID), not the registered client's ID. Both IDs are unfortunately prefixed with client_ lol
Valuable references for the reviewer:
demovideo.mp4
PR Description
Adds OAuth 2.1 Resource Server authentication to arcade-mcp-server, enabling HTTP MCP servers to validate Bearer tokens on every request. This unlocks tool-level authorization and secrets support for HTTP servers.
Testing
Beyond the comprehensive unit tests, I also manually tested end-to-end with WorkOS Authkit (DCR) and KeyCloak (non-DCR).
Future Work
ArcadeResourceServerto make adding front-door auth super easy when using Arcade's Auth ServerNote
Adds OAuth 2.1 front-door auth (JWKS validation + OAuth discovery) and propagates user identity to tools, enabling auth/secret-requiring tools over HTTP.
resource_servermodule withResourceServerAuth(multi-authorization-server, metadata) andJWKSTokenValidator(JWKS-based JWT validation) plus granular validation options.ResourceServerMiddlewarevalidates Bearer tokens on every HTTP request and injectsresource_owner./.well-known/oauth-protected-resource[/<path>].MCPApp/workeracceptauth/resource_server_validator, mount middleware, expose discovery; logs accepted auth servers.http_streamable) carriesSessionMessagewithresource_ownerfrom request → session.Context/Session/Serverplumbresource_owner;Serverselectsuser_idpreferring tokensub.authorization/secretswhen request is authenticated; otherwise blocked with actionable error.MCP_RESOURCE_SERVER_*inMCPSettings.ResourceServerSettings;.envauto-load.resource_server_typeon server start.examples/mcp_servers/authorizationsample server (HTTP auth, secrets, Reddit tool) with Docker setup.arcade-mcp-serverto1.12.0; minor docstring tweak in__init__.py.Written by Cursor Bugbot for commit d1116cd. This will update automatically on new commits. Configure here.
Resolves TOO-152