-
Notifications
You must be signed in to change notification settings - Fork 426
Closed
Copy link
Labels
bugSomething isn't workingSomething isn't workingtriageIssues / Features awaiting triageIssues / Features awaiting triage
Milestone
Description
The MCP Gateway's OAuth implementation isn't complete - where an authenticated user can access any other user's OAuth tokens. When User A invokes a tool from an OAuth-protected MCP server, the gateway may execute the request using User B's OAuth credentials, allowing unauthorized access to User B's resources.
Current Behavior
When a user invokes a tool from an OAuth-protected MCP server (registered as a "Gateway"):
- The gateway calls
token_storage.get_any_valid_token(gateway.id) - This returns the first available token for that gateway, regardless of which user owns it
- User A's request executes with User B's OAuth credentials
- User A gains unauthorized access to User B's resources (e.g., private GitHub repos)
Expected Behavior
Each user's OAuth tokens should be completely isolated:
- User A's requests should only use User A's OAuth tokens
- If User A hasn't authorized a gateway, they should receive an error prompting authorization
- No cross-user token access should be possible
Root Causes
1. No User Context Propagation
- The
/rpcendpoint authenticates users but doesn't pass identity to service layer - Service methods (
ToolService.invoke_tool()) don't accept user parameters - No way to filter tokens by requesting user
2. Missing User Mapping
- OAuth tokens store only the OAuth provider's user ID (e.g., "github-user-123")
- No
app_user_emailfield linking tokens to MCP Gateway users - Database schema lacks user association
3. Insecure OAuth Flow
/oauth/authorize/{gateway_id}doesn't require authentication- State parameter is just
gateway_id_randomwith no user info or integrity check - OAuth callback has no way to determine which MCP user initiated the flow
4. Authorization Header Collision
- Gateway consumes
Authorizationheader for its own JWT validation - Prevents forwarding of bearer tokens to upstream MCP servers
- Blocks non-OAuth authentication methods to external servers
Affected Code
Token Retrieval (Multiple Locations)
# mcpgateway/services/token_storage_service.py:167
async def get_any_valid_token(self, gateway_id: str) -> Optional[str]:
token_record = self.db.execute(
select(OAuthToken).where(OAuthToken.gateway_id == gateway_id)
).scalar_one_or_none() # Returns ANY user's token!
# mcpgateway/services/tool_service.py:954-956
# Try to get a valid token for any user (for now, we'll use a placeholder)
# In a real implementation, you might want to specify which user's tokens to use
access_token = await token_storage.get_any_valid_token(gateway.id)
# mcpgateway/services/gateway_service.py
# Lines 753, 1568, 1655, 1834 all use get_any_valid_token()OAuth Flow
# mcpgateway/routers/oauth_router.py
@oauth_router.get("/authorize/{gateway_id}")
async def initiate_oauth_flow(..., db: Session = Depends(get_db)):
# No authentication required!
# No user context in state parameter!Service Layer
# mcpgateway/main.py
@utility_router.post("/rpc")
async def handle_rpc(..., user=Depends(require_auth)):
# Has user but doesn't pass it to services
result = await tool_service.invoke_tool(
db=db, name=name, arguments=arguments, request_headers=headers
# Missing: app_user_email=user.email
)Proposed Solution
1. Add User Association to OAuth Tokens
- Add
app_user_emailcolumn tooauth_tokenstable with foreign key toemail_users - Create unique constraint on
(gateway_id, app_user_email) - Delete existing tokens (they lack user context)
2. Replace Token Retrieval Logic
- Delete
get_any_valid_token()entirely - Implement
get_user_token(gateway_id, app_user_email)with strict user filtering - Return
Noneif user hasn't authorized the gateway
3. Propagate User Context
- Add
app_user_emailparameter to all service methods - Pass authenticated user from API layer to service layer
- Ensure all entry points (
/rpc,/mcp, SSE, WebSocket) provide user context
4. Secure OAuth Flow
- Require authentication on
/oauth/authorize/{gateway_id} - Encode user email in state parameter with HMAC signature
- Validate state integrity in callback
- Store user association when saving tokens
5. Resolve Authorization Header Conflict
- Support alternate header (e.g.,
X-MCP-Gateway-Auth) for gateway authentication - Allow
Authorizationheader passthrough when gateway uses alternate auth - Document cookie-only authentication option
Impact
Security
- Critical: Prevents unauthorized access to other users' OAuth-protected resources
- Ensures proper multi-tenant isolation
- Provides audit trail of which user authorized which gateway
Breaking Changes
- All existing OAuth tokens must be deleted (they lack user context)
- Users must re-authorize OAuth gateways after the fix
- API behavior changes to require user context
Files Affected
- Database migration (new Alembic version)
mcpgateway/services/token_storage_service.pymcpgateway/services/tool_service.pymcpgateway/services/gateway_service.pymcpgateway/services/oauth_manager.pymcpgateway/routers/oauth_router.pymcpgateway/main.pymcpgateway/db.py- Transport implementations (SSE, WebSocket, Streamable HTTP)
- Authentication utilities and header handling
Testing Requirements
def test_oauth_user_isolation():
"""Verify tokens are isolated per user."""
# Alice authorizes GitHub
await token_storage.store_tokens(
gateway_id="github-gateway",
app_user_email="[email protected]",
access_token="alice_token"
)
# Bob cannot use Alice's token
token = await token_storage.get_user_token(
gateway_id="github-gateway",
app_user_email="[email protected]"
)
assert token is None
# Alice gets her own token
token = await token_storage.get_user_token(
gateway_id="github-gateway",
app_user_email="[email protected]"
)
assert token == "alice_token"Related Issues
- [Feature Request]: OAuth Enhancement following PR 768 #782 - OAuth Enhancement (mentions this as a TODO but doesn't address it)
- Authorization header collision prevents non-OAuth bearer token passthrough
Acceptance Criteria
-
get_any_valid_token()is completely removed - All OAuth tokens are user-scoped via
app_user_email - OAuth authorization requires authenticated user
- State parameter is signed and includes user identity
- User context propagates from API to service layer
- Tests verify complete user isolation
- Documentation updated with security implications
Additional Context
The comment in the code acknowledges this is a placeholder:
"Try to get a valid token for any user (for now, we'll use a placeholder)
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingtriageIssues / Features awaiting triageIssues / Features awaiting triage