This PR fixes multiple OAuth-related issues: ## Unified OAuth Callback - Consolidated `/oauth/callback-nextcloud` and `/oauth/login-callback` into single `/oauth/callback` endpoint - Flow type determined by session lookup via state parameter (no query params in redirect_uri) - Fixes redirect_uri validation issues with IdPs requiring exact match - Legacy endpoints kept as aliases for backwards compatibility ## PKCE Implementation - Implemented PKCE (RFC 7636) for Flow 2 (resource provisioning) - Generate code_verifier and code_challenge - Store code_verifier in session storage - Retrieve and use in token exchange - Fixed PKCE for browser login (integrated mode) - Previously only worked for external IdP (Keycloak) - Now works for both Nextcloud OIDC and external IdP ## Login Elicitation Fixes (ADR-006) - Fixed elicitation URL to route through MCP server endpoint - Changed from direct Nextcloud URL to `/oauth/authorize-nextcloud` - Ensures PKCE is properly handled by server - Fixed login detection after OAuth flow completes - Look up refresh token by state parameter instead of user_id - Works even when Flow 1 token not present - Added `get_refresh_token_by_provisioning_client_id()` method ## Session Authentication - Fixed `/user/page` redirect loop - Shared oauth_context with mounted browser_app - SessionAuthBackend can now validate sessions correctly ## Tests - Added comprehensive login elicitation test suite - Updated scope authorization test expectations - All 43 OAuth tests passing ## Files Changed - `app.py`: Shared oauth_context, unified callback route - `oauth_routes.py`: Unified callback, PKCE for Flow 2 - `browser_oauth_routes.py`: PKCE for integrated mode - `oauth_tools.py`: Fixed elicitation URL generation - `refresh_token_storage.py`: Added lookup by provisioning_client_id - `test_login_elicitation.py`: New test suite 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
29 KiB
ADR-006: Progressive Consent via URL Elicitation (SEP-1036)
Status: Partially Implemented (Interim Workaround) Date: 2025-01-05 (Updated: 2025-01-07) Related: SEP-1036, ADR-004 Depends On: ADR-005 (token validation)
Context
What is Progressive Consent?
Progressive consent is a mechanism, not a feature. It describes HOW users grant the MCP server access to Nextcloud resources through OAuth elicitation. The server can operate in two modes:
-
Pass-through mode (ENABLE_OFFLINE_ACCESS=false):
- No refresh tokens requested or stored
- Server passes through client's access token to Nextcloud
- No provisioning tools available
- Suitable for stateless, client-driven operations
-
Offline access mode (ENABLE_OFFLINE_ACCESS=true):
- Server requests
offline_accessscope and stores refresh tokens - Enables background operations and server-initiated API calls
- Provisioning tools available (
provision_nextcloud_access,check_logged_in) - Requires explicit user consent via OAuth Flow 2
- Server requests
Single-user mode (BasicAuth) doesn't use progressive consent at all - credentials are directly available.
Current User Experience Issues
The current offline access provisioning flow (ADR-004) requires users to manually visit OAuth URLs returned by MCP tools. This creates a poor user experience:
- User calls
provision_nextcloud_accesstool - Tool returns a URL as text in the response
- User must manually copy URL and open in browser
- No indication when provisioning is complete
- User must retry the original operation manually
SEP-1036: URL Mode Elicitation
The MCP specification now supports URL mode elicitation (SEP-1036), which enables servers to:
- Request out-of-band user interactions via secure URLs
- Handle sensitive operations like OAuth flows without exposing credentials to the client
- Provide progress tracking for async operations
- Return errors that automatically trigger elicitation flows
Key benefits for progressive consent:
- Automatic URL Opening: Client opens URL in browser automatically (with user consent)
- Progress Tracking: Server can notify client when provisioning is complete
- Error-Triggered Flows: Server can return
ElicitationRequirederror to trigger provisioning - Better UX: User doesn't manually copy/paste URLs
Current Implementation Limitations
The current progressive consent flow in nextcloud_mcp_server/server/oauth_tools.py:
@mcp.tool(name="provision_nextcloud_access")
async def tool_provision_access(ctx: Context) -> ProvisioningResult:
"""Returns OAuth URL as text - user must manually open it."""
return ProvisioningResult(
success=True,
authorization_url=auth_url, # User must copy this
message="Please visit the authorization URL..."
)
Problems:
- Manual URL handling (copy/paste)
- No progress tracking
- No automatic retry after provisioning
- Tool call required just to get URL
- No client integration (URL just displayed as text)
Decision
We will migrate progressive consent from manual tools to URL mode elicitation, leveraging SEP-1036 for better user experience and OAuth security.
New Architecture: Elicitation-Driven Consent
Instead of explicit tools, use automatic elicitation triggered by authorization errors:
User → Calls Nextcloud Tool → Server Checks Provisioning
↓ Not Provisioned
Error: ElicitationRequired
↓
Client Shows Consent UI
↓ User Accepts
Client Opens OAuth URL
↓
User Completes OAuth
↓
Server Sends Progress Update
↓
Original Tool Call Auto-Retries
Mode 1: Elicitation-Required Error (Primary)
When a tool requires provisioning, return an ElicitationRequired error (-32000):
# In any Nextcloud tool decorated with @require_provisioning
@mcp.tool()
@require_provisioning # New decorator
async def nc_notes_list_notes(ctx: Context):
"""List notes - auto-triggers provisioning if needed."""
# If not provisioned, decorator returns ElicitationRequired error
# If provisioned, continues normally
client = await get_client(ctx)
return await client.notes.list_notes()
Error response structure:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Nextcloud access provisioning required",
"data": {
"elicitations": [
{
"mode": "url",
"elicitationId": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://mcp.example.com/oauth/provision?id=550e8400...",
"message": "Grant the MCP server access to your Nextcloud account to continue."
}
]
}
}
}
Client behavior:
- Receives error with elicitation
- Shows consent UI: "App wants to access Nextcloud. Open authorization page?"
- On user acceptance, opens URL in browser
- Optionally tracks progress via
elicitation/track - Auto-retries original tool call when complete
Mode 2: Explicit Elicitation Request (Fallback)
For clients that don't support error-triggered elicitation, provide explicit tool:
@mcp.tool(name="request_nextcloud_access")
async def request_access(ctx: Context) -> ElicitationResponse:
"""Explicitly request provisioning via elicitation."""
# Send elicitation/create request
return await create_elicitation(
mode="url",
url=generate_oauth_url(),
message="Grant access to Nextcloud",
elicitation_id=generate_id()
)
Note: This is a fallback for compatibility. Primary flow uses error-triggered elicitation.
Implementation
1. New Decorator: @require_provisioning
Replace explicit provisioning checks with a decorator that returns ElicitationRequired:
# nextcloud_mcp_server/auth/provisioning_decorator.py
def require_provisioning(func):
"""
Decorator that ensures user has provisioned Nextcloud access.
If not provisioned, returns ElicitationRequired error with OAuth URL.
Otherwise, proceeds with normal tool execution.
"""
@functools.wraps(func)
async def wrapper(ctx: Context, *args, **kwargs):
# Extract user ID from token
user_id = get_user_id_from_context(ctx)
# Check if provisioned
storage = RefreshTokenStorage.from_env()
await storage.initialize()
if not await storage.has_refresh_token(user_id):
# Not provisioned - return ElicitationRequired error
elicitation_id = str(uuid.uuid4())
oauth_url = await generate_oauth_url_for_provisioning(
user_id=user_id,
elicitation_id=elicitation_id,
ctx=ctx
)
# Store elicitation for tracking
await storage.store_elicitation(
elicitation_id=elicitation_id,
user_id=user_id,
status="pending",
created_at=datetime.now(timezone.utc)
)
raise McpError(
code=ErrorCode.ELICITATION_REQUIRED, # -32000
message="Nextcloud access provisioning required",
data={
"elicitations": [
{
"mode": "url",
"elicitationId": elicitation_id,
"url": oauth_url,
"message": (
"Grant the MCP server access to your Nextcloud "
"account to continue. This is a one-time setup."
)
}
]
}
)
# Already provisioned - proceed normally
return await func(ctx, *args, **kwargs)
return wrapper
2. Elicitation Tracking Endpoint
Implement elicitation/track to provide progress updates:
# nextcloud_mcp_server/server/elicitation.py
@mcp.request_handler("elicitation/track")
async def track_elicitation(
elicitation_id: str,
_meta: dict = None
) -> dict:
"""
Track progress of an elicitation request.
Returns when elicitation is complete or times out.
"""
progress_token = _meta.get("progressToken") if _meta else None
storage = RefreshTokenStorage.from_env()
await storage.initialize()
# Poll for completion (with timeout)
timeout = 300 # 5 minutes
start_time = datetime.now(timezone.utc)
while (datetime.now(timezone.utc) - start_time).seconds < timeout:
elicitation = await storage.get_elicitation(elicitation_id)
if not elicitation:
raise McpError(
code=-32602, # Invalid params
message=f"Unknown elicitation ID: {elicitation_id}"
)
# Send progress notification if token provided
if progress_token and elicitation["status"] == "pending":
await send_progress_notification(
progress_token=progress_token,
progress=50,
message="Waiting for OAuth authorization..."
)
# Check if complete
if elicitation["status"] == "complete":
return {"status": "complete"}
# Check if failed
if elicitation["status"] == "failed":
return {
"status": "failed",
"error": elicitation.get("error_message")
}
# Wait before polling again
await asyncio.sleep(2)
# Timeout
raise McpError(
code=-32000,
message="Elicitation timed out - user did not complete authorization"
)
3. OAuth Callback Updates
Update the OAuth callback to mark elicitations as complete:
# nextcloud_mcp_server/auth/oauth_routes.py
async def oauth_callback(request: Request) -> Response:
"""Handle OAuth callback and mark elicitation complete."""
code = request.query_params.get("code")
state = request.query_params.get("state")
# Validate and exchange code for tokens
tokens = await exchange_authorization_code(code)
# Store refresh token
await storage.store_refresh_token(
user_id=user_id,
refresh_token=tokens["refresh_token"]
)
# Mark elicitation as complete
elicitation_id = request.query_params.get("elicitation_id")
if elicitation_id:
await storage.update_elicitation(
elicitation_id=elicitation_id,
status="complete",
completed_at=datetime.now(timezone.utc)
)
return Response(
content="<h1>Authorization Complete!</h1>"
"<p>You can close this window and return to the application.</p>",
media_type="text/html"
)
4. Update All Nextcloud Tools
Add @require_provisioning decorator to all Nextcloud tools:
# nextcloud_mcp_server/server/notes.py
@mcp.tool()
@require_scopes("notes:read")
@require_provisioning # NEW: Auto-triggers provisioning
async def nc_notes_list_notes(
ctx: Context,
category: Optional[str] = None
) -> NotesListResponse:
"""List all notes - automatically handles provisioning."""
client = await get_client(ctx)
# Tool logic proceeds only if provisioned
notes = await client.notes.list_notes(category=category)
return NotesListResponse(results=notes)
5. Capability Declaration
Declare URL elicitation support during initialization:
# nextcloud_mcp_server/app.py
capabilities = {
"elicitation": {
"url": {} # Declare URL mode support
# Note: We don't support "form" mode (in-band data collection)
},
# ... other capabilities
}
6. Environment Variables
Primary control:
# ENABLE_OFFLINE_ACCESS: Controls whether server requests refresh tokens and enables provisioning tools
# Default: false (pass-through mode)
# Set to true to enable offline access mode with Flow 2 provisioning
ENABLE_OFFLINE_ACCESS=true
Future variables (when URL elicitation is implemented):
# ELICITATION_CALLBACK_URL: Base URL for OAuth callbacks with elicitation tracking
# Default: NEXTCLOUD_MCP_SERVER_URL + /oauth/callback
ELICITATION_CALLBACK_URL=http://localhost:8000/oauth/callback
# ELICITATION_TIMEOUT_SECONDS: How long to wait for user to complete OAuth
# Default: 300 (5 minutes)
ELICITATION_TIMEOUT_SECONDS=300
Removed variables:
# ENABLE_PROGRESSIVE_CONSENT - Removed. Progressive consent is a mechanism, not a feature toggle.
# Use ENABLE_OFFLINE_ACCESS to control whether provisioning tools are available.
# MCP_SERVER_CLIENT_ID - merged into OIDC_CLIENT_ID
User Experience Comparison
Before (ADR-004 Manual Tools)
User: "List my notes"
Assistant: *calls nc_notes_list_notes*
Server: Error - not provisioned
Assistant: "You need to provision access first. Let me do that."
Assistant: *calls provision_nextcloud_access*
Server: {authorization_url: "https://..."}
Assistant: "Please visit this URL: https://..."
User: *copies URL, opens browser, completes OAuth*
User: "OK, I'm done"
Assistant: *calls nc_notes_list_notes again*
Server: Success! [notes...]
Issues: 4 interactions, manual URL handling, no automation
After (ADR-006 Elicitation)
User: "List my notes"
Assistant: *calls nc_notes_list_notes*
Server: ElicitationRequired error
Client: Shows dialog: "Grant access to Nextcloud? [Yes] [No]"
User: *clicks Yes*
Client: Opens OAuth URL in browser automatically
User: *completes OAuth*
Server: Sends progress notification "Complete!"
Client: Auto-retries nc_notes_list_notes
Server: Success! [notes...]
Assistant: "Here are your notes: ..."
Benefits: 1 interaction, automatic URL opening, seamless retry
Migration Path
Phase 1: Add Elicitation Support (v0.26.0)
- Implement
@require_provisioningdecorator - Add
elicitation/trackendpoint - Keep existing tools (
provision_nextcloud_access) for compatibility - Update OAuth callback to track elicitations
- Add capability declaration
Breaking changes: None (additive)
Phase 2: Update Documentation (v0.27.0)
- Document elicitation-based flow as primary
- Mark manual tools as deprecated
- Update examples and guides
Breaking changes: None (documentation only)
Phase 3: Remove Manual Tools (v0.28.0)
- Remove
provision_nextcloud_accesstool - Remove
check_provisioning_statustool (status in error message) - Remove
revoke_nextcloud_access(or keep for explicit revocation?)
Breaking changes: Yes (removed tools)
Phase 4: Optimize (v0.29.0+)
- Add elicitation result caching
- Implement retry strategies
- Add metrics and monitoring
Testing
Test Cases
-
First-Time User Flow
@pytest.mark.oauth async def test_elicitation_first_time_user(nc_mcp_oauth_client): """Test that first tool call triggers elicitation.""" # User has no provisioning with pytest.raises(McpError) as exc: await nc_mcp_oauth_client.call_tool("nc_notes_list_notes") # Should get ElicitationRequired error assert exc.value.code == -32000 assert "elicitations" in exc.value.data assert exc.value.data["elicitations"][0]["mode"] == "url" # Verify URL is valid OAuth URL url = exc.value.data["elicitations"][0]["url"] assert "oauth" in url assert "elicitationId" in url -
Progress Tracking
@pytest.mark.oauth async def test_elicitation_progress_tracking(nc_mcp_oauth_client): """Test progress tracking during OAuth flow.""" # Trigger elicitation elicitation_id = trigger_elicitation() # Start tracking track_task = asyncio.create_task( nc_mcp_oauth_client.track_elicitation( elicitation_id=elicitation_id, progress_token="test-token" ) ) # Simulate OAuth completion await asyncio.sleep(1) await complete_oauth_flow(elicitation_id) # Track should complete result = await track_task assert result["status"] == "complete" -
Auto-Retry After Provisioning
@pytest.mark.oauth async def test_auto_retry_after_provisioning(nc_mcp_oauth_client): """Test that client auto-retries after elicitation.""" # Mock client that auto-retries on ElicitationRequired client = AutoRetryMcpClient(nc_mcp_oauth_client) # First call triggers elicitation, client handles it, retries result = await client.call_tool_with_elicitation("nc_notes_list_notes") # Should succeed after provisioning assert result.success assert "notes" in result.data -
Timeout Handling
@pytest.mark.oauth async def test_elicitation_timeout(nc_mcp_oauth_client): """Test timeout if user doesn't complete OAuth.""" elicitation_id = trigger_elicitation() # Track with short timeout with pytest.raises(McpError, match="timed out"): await nc_mcp_oauth_client.track_elicitation( elicitation_id=elicitation_id, timeout=5 # 5 seconds )
Security Considerations
Out-of-Band OAuth Flow
Benefit: OAuth credentials never pass through MCP client
- User enters credentials directly on IdP page
- MCP server receives only authorization code
- Client never sees passwords or refresh tokens
Threat mitigation:
- Credential theft: Client can't intercept credentials (out-of-band)
- Token exposure: Client never receives Nextcloud refresh tokens
- CSRF: State parameter validates OAuth callback
- URL tampering: Elicitation ID ties OAuth flow to user session
Elicitation ID as Security Token
The elicitationId serves as a capability token:
- Cryptographically random (UUID v4)
- Single-use (invalidated after completion)
- Time-limited (expires after timeout)
- User-scoped (tied to user session)
Validation:
async def validate_elicitation_id(elicitation_id: str, user_id: str) -> bool:
"""Validate that elicitation belongs to user and is still valid."""
elicitation = await storage.get_elicitation(elicitation_id)
if not elicitation:
return False
# Check ownership
if elicitation["user_id"] != user_id:
logger.warning(f"Elicitation ID mismatch: {elicitation_id}")
return False
# Check expiry
if elicitation["expires_at"] < datetime.now(timezone.utc):
return False
# Check not already used
if elicitation["status"] != "pending":
return False
return True
Progress Tracking Security
Risk: Progress token reuse across users
Mitigation:
- Progress tokens tied to elicitation ID
- Elicitation ID tied to user session
- Server validates ownership before sending updates
Consequences
Positive
- Better UX: Automatic URL opening, no manual copy/paste
- Seamless Flow: Auto-retry after provisioning
- Progress Feedback: User knows when OAuth is complete
- Spec Compliance: Implements SEP-1036 correctly
- Secure by Design: Out-of-band OAuth prevents credential exposure
- Simpler API: No explicit provisioning tools needed
Negative
- Client Dependency: Requires client support for URL elicitation
- Complexity: More moving parts (elicitation tracking, callbacks)
- Polling: Progress tracking uses polling (not ideal)
- Breaking Change: Removes manual provisioning tools (in v0.28.0)
Neutral
- Storage Requirements: Need to store elicitation state
- Timeout Management: Must handle long-running OAuth flows
- Fallback Support: Still need compatibility for older clients
Alternatives Considered
1. Keep Manual Tools Only (Rejected)
Pros: Simple, no client changes needed Cons: Poor UX, doesn't leverage SEP-1036
Rejection reason: SEP-1036 provides better UX and security
2. Form Mode Elicitation (Rejected)
Pros: No browser redirect needed Cons: Would expose OAuth credentials to client (security violation)
Rejection reason: Form mode only for non-sensitive data per SEP-1036
3. Hybrid: Both Tools and Elicitation (Considered)
Pros: Maximum compatibility, gradual migration Cons: API duplication, maintenance burden, confusing for users
Decision: Support during migration (v0.26-0.27), remove in v0.28
4. WebSocket for Progress (Rejected)
Pros: Real-time updates instead of polling Cons: MCP spec uses polling pattern, adds complexity
Rejection reason: Follow spec pattern (polling via elicitation/track)
Interim Implementation: Inline Form Elicitation (Pre-SEP-1036)
Note: SEP-1036 (URL mode elicitation) is not yet available in the stable MCP Python SDK. As a temporary workaround, we've implemented a simplified version using the current inline form elicitation API.
What Changed
Instead of waiting for URL mode elicitation, we implemented a check_logged_in tool that:
- Checks if the user has completed Flow 2 (resource provisioning)
- If logged in, returns
"yes" - If not logged in, uses inline form elicitation to prompt the user
Implementation Details
New Tool: check_logged_in
# nextcloud_mcp_server/server/oauth_tools.py
class LoginConfirmation(BaseModel):
"""Schema for login confirmation elicitation."""
acknowledged: bool = Field(
default=False,
description="Check this box after completing login at the provided URL",
)
@mcp.tool(name="check_logged_in")
@require_scopes("openid")
async def tool_check_logged_in(ctx: Context, user_id: Optional[str] = None) -> str:
"""Check if user is logged in and elicit login if needed."""
# Check if already logged in
status = await get_provisioning_status(ctx, user_id)
if status.is_provisioned:
return "yes"
# Generate OAuth URL for Flow 2
auth_url = generate_oauth_url_for_flow2(...)
# Use inline form elicitation (current MCP API)
result = await ctx.elicit(
message=f"Please log in to Nextcloud at the following URL:\n\n{auth_url}\n\nAfter completing the login, check the box below and click OK.",
schema=LoginConfirmation,
)
if result.action == "accept":
# Verify login succeeded
status = await get_provisioning_status(ctx, user_id)
return "yes" if status.is_provisioned else "Login not detected"
elif result.action == "decline":
return "Login declined by user."
else:
return "Login cancelled by user."
OAuth Routes (added to app.py):
# Flow 2 routes for resource provisioning
routes.append(
Route("/oauth/authorize-nextcloud", oauth_authorize_nextcloud, methods=["GET"])
)
routes.append(
Route("/oauth/callback-nextcloud", oauth_callback_nextcloud, methods=["GET"])
)
User Experience
User: *calls check_logged_in tool*
MCP Client: Displays form elicitation
┌─────────────────────────────────────────────────────────┐
│ Please log in to Nextcloud at the following URL: │
│ │
│ http://localhost:8000/oauth/authorize-nextcloud?... │
│ │
│ After completing the login, check the box below and │
│ click OK. │
│ │
│ ☐ Check this box after completing login │
│ │
│ [Accept] [Decline] [Cancel] │
└─────────────────────────────────────────────────────────┘
User: *copies URL, opens in browser, completes OAuth*
User: *checks box and clicks Accept*
MCP Server: Verifies login and returns "yes"
Limitations of Interim Approach
- Manual URL Handling: User must manually copy and paste the URL (not clickable)
- No Automatic Browser Opening: Client doesn't automatically open the URL
- No Progress Tracking: Can't track OAuth completion status in real-time
- URL in Message Text: Login URL embedded in plain text message (not as structured field)
- Client-Side Confirmation: Relies on user clicking "OK" after OAuth (honor system)
Why Not Use URL Mode Now?
The current stable MCP Python SDK (main branch) only supports inline form elicitation:
# Current API (no 'mode' parameter)
class ElicitRequestParams(RequestParams):
message: str
requestedSchema: ElicitRequestedSchema
# No 'mode', 'url', or 'elicitationId' fields
URL mode elicitation (mode: "url") is only available in the SEP-1036 branch, which has not been merged to main yet.
Migration to URL Mode (When SEP-1036 Lands)
Once SEP-1036 is merged and available in the stable SDK, we will migrate to URL mode elicitation:
Before (Current Workaround):
result = await ctx.elicit(
message=f"Please log in at: {auth_url}\n\nClick OK after login.",
schema=LoginConfirmation,
)
After (URL Mode):
result = await ctx.session.elicit_url(
message="Please log in to Nextcloud to authorize this MCP server.",
url=auth_url,
elicitation_id=elicitation_id,
)
Benefits of migration:
- Automatic URL opening (with user consent)
- Clickable URLs in client UI
- Progress tracking via
elicitation/track - Better security (URL not in message text)
- Auto-retry support
Testing
Integration tests validate the current inline form elicitation:
# tests/server/oauth/test_login_elicitation.py
async def test_check_logged_in_already_authenticated(nc_mcp_oauth_client):
"""Test immediate 'yes' for authenticated users."""
result = await nc_mcp_oauth_client.call_tool("check_logged_in", arguments={})
assert "yes" in result.content[0].text.lower()
async def test_check_logged_in_url_format(nc_mcp_oauth_client):
"""Test that login URL (when needed) contains correct OAuth parameters."""
result = await nc_mcp_oauth_client.call_tool("check_logged_in", arguments={})
response_text = result.content[0].text
# If URL present, validate OAuth parameters
if "http" in response_text:
assert "response_type=code" in response_text
assert "client_id=" in response_text
assert "redirect_uri=" in response_text
assert "openid" in response_text
Future Work
- Monitor SEP-1036: Watch for merge to MCP Python SDK
mainbranch - Implement URL Mode: Once available, migrate
check_logged_into usectx.session.elicit_url() - Add Progress Tracking: Implement
elicitation/trackendpoint for OAuth completion status - Implement Error-Triggered Elicitation: Use
@require_provisioningdecorator to returnElicitationRequirederrors - Remove Manual Workaround: Deprecate inline form approach once URL mode is stable
References
- SEP-1036: URL Mode Elicitation
- MCP Elicitation Specification
- ADR-004: Federated Authentication Architecture
- ADR-005: Token Audience Validation
- RFC 8252: OAuth 2.0 for Native Apps
Implementation Checklist
Interim Implementation (Inline Form Elicitation)
- Create
check_logged_intool with inline form elicitation - Register Flow 2 OAuth routes (
/oauth/authorize-nextcloud,/oauth/callback-nextcloud) - Write integration tests for login elicitation flow
- Update ADR-006 with interim implementation documentation
- Add
LoginConfirmationschema for elicitation - Run tests to validate implementation
Future Work (URL Mode Elicitation - Post SEP-1036)
- Implement
@require_provisioningdecorator with ElicitationRequired error - Add
elicitation/trackrequest handler - Update OAuth callback to mark elicitations complete
- Add elicitation storage (ID, user, status, timestamps)
- Update all Nextcloud tools with
@require_provisioning - Add URL elicitation capability declaration
- Write tests for progress tracking
- Update documentation with URL mode examples
- Add migration guide for manual tools → elicitation
- Migrate
check_logged_infrom inline form to URL mode - Keep manual tools with deprecation warnings (v0.26-0.27)
- Remove manual tools (v0.28.0)
- Update CHANGELOG.md with migration timeline