Files
Chris Coutinho babd60e08b feat: Implement ADR-004 Hybrid Flow with comprehensive integration tests
Implement the ADR-004 Hybrid Flow OAuth pattern where the MCP server
intercepts the OAuth callback to obtain master refresh tokens while
maintaining PKCE security for clients.

## Implementation

### OAuth Routes (ADR-004 Hybrid Flow)
- Add `/oauth/authorize` endpoint: Intercepts client OAuth initiation
- Add `/oauth/callback` endpoint: Receives IdP callback, stores master token
- Add `/oauth/token` endpoint: Exchanges MCP code for client access token
- Implement PKCE code challenge/verifier validation
- Store OAuth sessions with state/challenge correlation

### MCP Server Integration
- Update `setup_oauth_config()` to return client_id and client_secret
- Initialize OAuth context in Starlette lifespan for login routes
- Add OAuth session storage to RefreshTokenStorage
- Configure authlib dependency for OAuth flow management

### Integration Tests
- Create `test_adr004_hybrid_flow.py` with Playwright automation
- Add `adr004_hybrid_flow_mcp_client` session-scoped fixture
- Test MCP session establishment with hybrid flow token
- Test tool execution using stored refresh tokens (on-behalf-of pattern)
- Test persistent access across multiple operations
- All tests passing:  3 passed in 8.82s

### Documentation
- Update ADR-004 with comprehensive Testing section
- Add integration test commands and coverage details
- Document test implementation and verification steps
- Create TESTING_INSTRUCTIONS.md for manual and automated testing
- Include manual test scripts for reference/debugging

## What This Enables

 PKCE code challenge/verifier flow
 MCP server intercepts OAuth callback and stores master refresh token
 Client receives MCP access token (not master token)
 MCP session establishment with hybrid flow token
 Tool execution using stored refresh tokens (on-behalf-of pattern)
 Multiple operations without re-authentication
 Proper token isolation (client never sees master token)

## Testing

Run ADR-004 integration tests:
```bash
uv run pytest tests/server/oauth/test_adr004_hybrid_flow.py --browser firefox -v
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 02:18:30 +01:00

6.3 KiB

ADR-004 OAuth Flow Testing Instructions

The ADR-004 Hybrid Flow is now fully tested via automated integration tests using Playwright:

# Run all ADR-004 tests
uv run pytest tests/server/oauth/test_adr004_hybrid_flow.py --browser firefox -v

# Run specific test
uv run pytest tests/server/oauth/test_adr004_hybrid_flow.py::test_adr004_hybrid_flow_tool_execution --browser firefox -v

These tests verify:

  • PKCE code challenge/verifier flow
  • MCP server intercepts OAuth callback
  • Master refresh token storage
  • Client receives MCP access token
  • MCP session establishment with hybrid flow token
  • Tool execution using stored refresh tokens
  • Multiple operations without re-authentication

Manual Test (Legacy)

For manual testing or debugging, you can use the standalone test script:

# Make sure port 8765 is available
lsof -ti:8765 | xargs kill -9 2>/dev/null

# Run the test
uv run python tests/manual/test_adr004_manual.py --provider nextcloud

Expected Flow

1. Test Script Starts

======================================================================
ADR-004 MANUAL OAUTH FLOW TEST
======================================================================
Provider:          nextcloud
MCP Server:        http://localhost:8001
Nextcloud:         http://localhost:8080
======================================================================

✓ Generated PKCE challenge: gxQLsYDJ...
✓ Started callback server at http://localhost:8765/callback

2. Open OAuth URL in Browser

The script will print:

======================================================================
STEP 1: AUTHORIZE THE MCP SERVER
======================================================================

📋 Open this URL in your browser:

    http://localhost:8001/oauth/authorize?response_type=code&...

📌 What will happen:
   1. You'll be redirected to Nextcloud/Keycloak login
   2. Login with username: admin, password: admin
   3. You'll see a consent screen asking to authorize the MCP server
   4. Click 'Authorize' or 'Allow'
   5. You'll be redirected to localhost:8765/callback
   6. The authorization code will appear in the terminal

3. Browser Flow

  1. Nextcloud Login - You see the Nextcloud login page
  2. Enter Credentials - admin/admin
  3. Consent Screen - "Authorize Nextcloud MCP Server (jwt) to access your account?"
  4. Click Authorize
  5. Redirect Chain:
    • Nextcloud redirects to: http://localhost:8001/oauth/callback?code=...
    • MCP server processes the code
    • MCP server redirects to: http://localhost:8765/callback?code=mcp-code-...&state=...
    • Browser reaches the test script's callback server
    • You see: "✓ Authorization Successful - You can close this window"

4. Test Script Continues

✓ Received authorization code!
Code: mcp-code-xyz...
✓ State parameter verified (CSRF protection)

======================================================================
STEP 2: EXCHANGE CODE FOR ACCESS TOKEN
======================================================================

✓ Successfully received access token
  Token: eyJhbGciOiJSUzI1Ni...
  Type: Bearer
  Expires: 3600s

======================================================================
STEP 3: CALL MCP TOOL WITH ACCESS TOKEN
======================================================================

✓ MCP tool call succeeded!
  Result: {...}

======================================================================
🎉 ADR-004 OAUTH FLOW TEST - SUCCESS
======================================================================

Troubleshooting

Browser Gets Stuck at "localhost:8765 refused to connect"

Problem: The callback server on port 8765 isn't accessible.

Solutions:

  1. Check firewall isn't blocking port 8765
  2. Verify the test script is still running
  3. Check another process isn't using port 8765:
    lsof -ti:8765
    

Browser Shows "localhost:8765 - ERR_CONNECTION_REFUSED"

Problem: The callback server stopped or never started.

Solution:

  1. Check the test script output - it should say "✓ Started callback server"
  2. Restart the test script
  3. Manually test the callback server:
    curl http://localhost:8765/callback?code=test&state=test
    
    Should return HTML page with "Authorization Successful"

"Session not found or expired" Error

Problem: Took too long between steps (>10 minutes).

Solution: Restart the test - sessions expire after 10 minutes.

Client ID is None

Problem: OAuth client credentials not loaded.

Solution: Rebuild the MCP server:

docker-compose up --build -d mcp-oauth

Nextcloud Shows "Invalid redirect_uri"

Problem: The redirect URI isn't registered for the OAuth client.

Solution: Check registered URIs:

docker compose exec db mariadb -u root -ppassword nextcloud -e \
  "SELECT c.client_identifier, r.redirect_uri FROM oc_oidc_clients c \
   LEFT JOIN oc_oidc_redirect_uris r ON c.id = r.client_id \
   WHERE c.name LIKE '%MCP%';"

Should show: http://localhost:8001/oauth/callback

Manual Test Without Script

If the automated test doesn't work, you can test manually:

  1. Start callback server manually:

    python3 -m http.server 8765
    
  2. Open OAuth URL in browser (get from test script output or build manually):

    http://localhost:8001/oauth/authorize?response_type=code&client_id=test-mcp-client&redirect_uri=http://localhost:8765/callback&scope=openid+profile+email+offline_access&state=TEST&code_challenge=CHALLENGE&code_challenge_method=S256
    
  3. Complete login at Nextcloud

  4. Browser should redirect to http://localhost:8765/callback?code=mcp-code-...&state=TEST

  5. Copy the code from the URL and exchange it:

    curl -X POST http://localhost:8001/oauth/token \
      -d "grant_type=authorization_code" \
      -d "code=<MCP_CODE_HERE>" \
      -d "code_verifier=<VERIFIER_HERE>" \
      -d "redirect_uri=http://localhost:8765/callback" \
      -d "client_id=test-mcp-client"
    

Expected Database State After Success

# Check refresh token was stored
docker compose exec mcp-oauth sh -c \
  "sqlite3 /app/data/tokens.db 'SELECT user_id, created_at FROM refresh_tokens;'"

Should show an entry for the authenticated user.