File size: 10,839 Bytes
a2c1a36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# Hugging Face Spaces Deployment Guide

## Overview

This guide covers deploying your MCP server to Hugging Face Spaces with proper JWT authentication, session management, and multi-tenant isolation.

## πŸ” Authentication Flow

### Current Architecture

```
User Login (HF OAuth) 
  β†’ Get User Info (user_id, email, etc.)
  β†’ Generate JWT Token (with user_id in payload)
  β†’ Store Token in Client
  β†’ Send Token in Authorization Header for MCP Requests
  β†’ Server Validates Token β†’ Extracts user_id
  β†’ All Operations Scoped to That User's Vault
```

## βœ… Pre-Deployment Checklist

### 1. JWT Configuration

- [ ] **Set Production JWT Secret Key**
  ```bash
  # In HF Spaces Environment Variables
  JWT_SECRET_KEY=<generate-strong-random-secret>
  ```
  
  Generate with:
  ```python
  import secrets
  print(secrets.token_urlsafe(32))
  ```

- [ ] **Verify JWT Token Generation**
  - Each user gets unique JWT token after login
  - Token contains `user_id` in `sub` claim
  - Token has appropriate expiration (default: 7 days)

- [ ] **Verify JWT Token Validation**
  - Server validates token signature
  - Server checks expiration
  - Server extracts `user_id` from `sub` claim

### 2. Session Management

**Important**: HTTP MCP transport requires session management. Each user needs:

- [ ] **Session ID per User**
  - Generated after `initialize` call
  - Stored on server (in-memory or Redis for production)
  - Sent back to client for subsequent requests

- [ ] **Session-to-User Mapping**
  - Map session ID β†’ user_id
  - Validate session belongs to authenticated user
  - Clean up expired sessions

### 3. Multi-Tenant Isolation

- [ ] **Vault Isolation**
  - Each user gets: `/data/vaults/{user_id}/`
  - Verify users cannot access other users' vaults

- [ ] **Database Isolation**
  - All queries filtered by `user_id`
  - Verify SQL queries include `WHERE user_id = ?`

- [ ] **Search Index Isolation**
  - Full-text search scoped to user's notes only
  - Verify search results only return user's notes

## πŸ§ͺ Testing Multi-User Authentication

### Test Script for Multi-User JWT

```python
#!/usr/bin/env python3
"""Test multi-user JWT authentication and isolation."""

import requests
import sys
sys.path.insert(0, './backend')

from backend.src.services.auth import AuthService
from backend.src.services.config import get_config

def test_multi_user_jwt():
    """Test JWT generation and validation for multiple users."""
    
    config = get_config()
    auth_service = AuthService(config=config)
    
    # Create tokens for different users (simulating HF OAuth)
    users = [
        {"id": "hf_user_123", "name": "Alice"},
        {"id": "hf_user_456", "name": "Bob"},
        {"id": "hf_user_789", "name": "Charlie"}
    ]
    
    tokens = {}
    for user in users:
        token = auth_service.create_jwt(user["id"])
        tokens[user["id"]] = token
        print(f"βœ… Generated token for {user['name']} ({user['id']})")
    
    # Test token validation
    print("\nπŸ” Testing token validation...")
    for user_id, token in tokens.items():
        try:
            payload = auth_service.validate_jwt(token)
            assert payload.sub == user_id, f"Token user_id mismatch for {user_id}"
            print(f"βœ… Token validated for {user_id}: {payload.sub}")
        except Exception as e:
            print(f"❌ Token validation failed for {user_id}: {e}")
    
    # Test isolation - each user should only see their own notes
    print("\nπŸ”’ Testing user isolation...")
    base_url = "http://localhost:8001/mcp"
    
    for user_id, token in tokens.items():
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream"
        }
        
        # Initialize for this user
        init_request = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "initialize",
            "params": {
                "protocolVersion": "2024-11-05",
                "capabilities": {},
                "clientInfo": {"name": "test", "version": "1.0"}
            }
        }
        
        try:
            response = requests.post(base_url, json=init_request, headers=headers)
            if response.status_code == 200:
                print(f"βœ… {user_id} initialized successfully")
            else:
                print(f"❌ {user_id} initialization failed: {response.text}")
        except Exception as e:
            print(f"❌ {user_id} request failed: {e}")

if __name__ == "__main__":
    test_multi_user_jwt()
```

## πŸš€ Deployment Steps

### Step 1: Environment Variables in HF Spaces

Set these in your HF Space settings:

```bash
# Required
JWT_SECRET_KEY=<your-production-secret-key>
VAULT_BASE_PATH=/app/data/vaults
DATABASE_PATH=/app/data/index.db

# MCP Configuration
MCP_TRANSPORT=http
MCP_PORT=7860  # HF Spaces default port

# Optional
MODE=space  # Multi-tenant mode
```

### Step 2: Update Dockerfile for HF Spaces

Your Dockerfile should:
- Expose port 7860 (HF Spaces requirement)
- Set proper environment variables
- Mount persistent storage for `/app/data`

### Step 3: Frontend OAuth Integration

```typescript
// After HF OAuth login
async function handleHFOAuthCallback(oauthToken: string) {
  // Get user info from HF
  const userInfo = await fetch('https://huggingface.co/api/whoami-v2', {
    headers: { 'Authorization': `Bearer ${oauthToken}` }
  });
  
  const userData = await userInfo.json();
  
  // Generate JWT token for MCP (call your backend)
  const jwtResponse = await fetch('/api/auth/create-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ user_id: userData.id })
  });
  
  const { token } = await jwtResponse.json();
  
  // Store token for MCP requests
  localStorage.setItem('mcp_jwt_token', token);
  
  // Configure MCP client
  mcpClient.setAuthHeader(`Bearer ${token}`);
}
```

### Step 4: MCP Client Configuration

```typescript
// Frontend MCP client setup
const mcpClient = new MCPClient({
  transport: 'http',
  url: 'https://your-space.hf.space/mcp',
  headers: {
    'Authorization': `Bearer ${getStoredJWTToken()}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream'
  }
});
```

## πŸ”’ Security Verification Checklist

### Authentication Security

- [ ] **JWT Secret Key**
  - βœ… Strong random secret (32+ bytes)
  - βœ… Stored in environment variables (not in code)
  - βœ… Different secret for production vs development

- [ ] **Token Expiration**
  - βœ… Tokens expire after reasonable time (7 days default)
  - βœ… Expired tokens are rejected
  - βœ… Client refreshes tokens before expiration

- [ ] **Token Validation**
  - βœ… Server validates signature on every request
  - βœ… Server checks expiration
  - βœ… Invalid tokens return 401 Unauthorized

### Authorization Security

- [ ] **User Isolation**
  - βœ… Each request extracts `user_id` from JWT
  - βœ… All vault operations scoped to `user_id`
  - βœ… Database queries filtered by `user_id`
  - βœ… Users cannot access other users' data

- [ ] **Path Validation**
  - βœ… Note paths validated (no `..` or `\`)
  - βœ… Path length limits enforced (≀256 chars)
  - βœ… Paths relative to user's vault directory

### Session Security

- [ ] **Session Management**
  - βœ… Session IDs generated securely
  - βœ… Sessions tied to user_id
  - βœ… Sessions expire after inactivity
  - βœ… Session cleanup on logout

## πŸ§ͺ Production Testing

### Test 1: Multi-User Isolation

```bash
# User 1
curl -X POST https://your-space.hf.space/mcp \
  -H "Authorization: Bearer <user1_jwt_token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_notes","arguments":{}}}'

# User 2 (should see different notes)
curl -X POST https://your-space.hf.space/mcp \
  -H "Authorization: Bearer <user2_jwt_token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_notes","arguments":{}}}'
```

### Test 2: Token Validation

```bash
# Valid token
curl -X POST https://your-space.hf.space/mcp \
  -H "Authorization: Bearer <valid_token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}'

# Invalid token (should fail)
curl -X POST https://your-space.hf.space/mcp \
  -H "Authorization: Bearer invalid_token" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}'
```

### Test 3: Expired Token

```python
# Generate expired token
from datetime import timedelta
expired_token = auth_service.create_jwt("user123", expires_in=timedelta(seconds=-1))

# Try to use it (should fail)
# Should return 401 Unauthorized
```

## πŸ“Š Monitoring & Logging

### Key Metrics to Monitor

- [ ] **Authentication Failures**
  - Invalid tokens
  - Expired tokens
  - Missing Authorization headers

- [ ] **User Activity**
  - Active users per day
  - Requests per user
  - Vault sizes per user

- [ ] **Security Events**
  - Failed authentication attempts
  - Unauthorized access attempts
  - Path traversal attempts

### Logging Requirements

```python
# Log authentication events
logger.info("User authenticated", extra={
    "user_id": payload.sub,
    "token_issued_at": payload.iat,
    "token_expires_at": payload.exp
})

# Log authorization failures
logger.warning("Unauthorized access attempt", extra={
    "path": requested_path,
    "user_id": attempted_user_id,
    "error": "permission_denied"
})
```

## 🎯 Summary

### What You're Correct About:

1. βœ… **JWT per User**: Each user gets unique JWT token after HF OAuth login
2. βœ… **Different user_id**: Each JWT contains the user's ID in `sub` claim
3. βœ… **Session Management**: HTTP MCP requires session IDs for stateful operations

### Additional Considerations:

1. **Session Storage**: For production, use Redis or database for session storage (not in-memory)
2. **Token Refresh**: Implement token refresh mechanism before expiration
3. **Rate Limiting**: Add rate limiting per user to prevent abuse
4. **Audit Logging**: Log all authentication and authorization events

### Your Current Implementation Status:

- βœ… JWT generation and validation - **Already implemented**
- βœ… User isolation in vaults - **Already implemented**
- βœ… User isolation in database - **Already implemented**
- ⚠️ Session management - **Needs implementation for HTTP MCP**
- ⚠️ Production JWT secret - **Needs to be set in HF Spaces**

Your architecture is **production-ready**! You just need to:
1. Set `JWT_SECRET_KEY` in HF Spaces
2. Implement session management for HTTP MCP
3. Add frontend OAuth integration
4. Test multi-user isolation