feat: add scope protection to OAuth provisioning tools
Add @require_scopes("openid") decorator to OAuth backend tools
(provision_nextcloud_access, revoke_nextcloud_access, check_provisioning_status)
to ensure they're only visible to authenticated OIDC users.
Design rationale:
- OAuth provisioning tools are "meta-tools" that manage authentication itself
- They don't access Nextcloud resources, so don't need resource scopes
- Requiring 'openid' ensures user is authenticated via OIDC
- Enables Progressive Consent: users authenticate first, then provision access
- Aligns with dual OAuth flow architecture (Flow 1 + Flow 2)
Changes:
- Add @require_scopes("openid") to all three OAuth provisioning tools
- Update test expectations: users with only OIDC default scopes
see OAuth provisioning tools but not resource tools
- All tests pass (13/13 in test_scope_authorization.py)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -394,11 +394,13 @@ async def test_jwt_with_no_custom_scopes_returns_zero_tools(
|
||||
nc_mcp_oauth_client_no_custom_scopes,
|
||||
):
|
||||
"""
|
||||
Test that a JWT token with only OIDC default scopes (no nc:read or nc:write) returns 0 tools.
|
||||
Test that a JWT token with only OIDC default scopes shows only OAuth provisioning tools.
|
||||
|
||||
This tests the security behavior when a user declines to grant custom scopes during consent.
|
||||
Expected: JWT token has scopes=['openid', 'profile', 'email'] but no nc:read or nc:write.
|
||||
All tools require at least one custom scope, so they should all be filtered out.
|
||||
Expected: JWT token has scopes=['openid', 'profile', 'email'] but no resource scopes.
|
||||
- Resource tools (notes:*, calendar:*, etc.) are filtered out
|
||||
- OAuth provisioning tools (requiring only 'openid') remain visible
|
||||
so users can provision Nextcloud access after authentication
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -410,16 +412,24 @@ async def test_jwt_with_no_custom_scopes_returns_zero_tools(
|
||||
|
||||
tool_names = [tool.name for tool in result.tools]
|
||||
logger.info(
|
||||
f"JWT token with no custom scopes sees {len(tool_names)} tools (should be 0)"
|
||||
f"JWT token with no custom scopes sees {len(tool_names)} tools (should be 3 OAuth tools)"
|
||||
)
|
||||
|
||||
# All tools require nc:read or nc:write, so should be filtered out
|
||||
assert len(tool_names) == 0, (
|
||||
f"Expected 0 tools but got {len(tool_names)}: {tool_names[:10]}"
|
||||
# Only OAuth provisioning tools should be visible (they require 'openid' scope)
|
||||
expected_oauth_tools = [
|
||||
"provision_nextcloud_access",
|
||||
"revoke_nextcloud_access",
|
||||
"check_provisioning_status",
|
||||
]
|
||||
|
||||
assert set(tool_names) == set(expected_oauth_tools), (
|
||||
f"Expected only OAuth provisioning tools {expected_oauth_tools} "
|
||||
f"but got {tool_names}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"✅ JWT token without custom scopes correctly returns 0 tools (all filtered out)"
|
||||
f"✅ JWT token with only openid scope correctly shows {len(tool_names)} OAuth provisioning tools, "
|
||||
"resource tools filtered out"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user