mdb-engine 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdb_engine/__init__.py CHANGED
@@ -82,15 +82,19 @@ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
82
82
  from .utils import clean_mongo_doc, clean_mongo_docs
83
83
 
84
84
  __version__ = (
85
- "0.5.0" # Major WebSocket security overhaul - cookie-based authentication
86
- # - BREAKING CHANGE: Removed subprotocol tunneling support
87
- # - NEW: Exclusive httpOnly cookie-based WebSocket authentication
88
- # - Comprehensive security guide (WEBSOCKET_SECURITY_MULTI_APP_SSO.md)
89
- # - Updated all documentation to reflect cookie-based authentication
90
- # - Enhanced CSRF protection for WebSocket upgrades
91
- # - Added integration tests for cookie-based WebSocket authentication
92
- # - Complete test coverage for WebSocket security scenarios
93
- # - Multi-app SSO compatibility with path="/" cookies
85
+ "0.6.0" # Secure-by-default WebSocket authentication with encrypted session keys
86
+ # - NEW: WebSocket session key generation and management
87
+ # - NEW: Envelope encryption for WebSocket session keys
88
+ # - NEW: Secure-by-default CSRF protection (csrf_required: true)
89
+ # - NEW: WebSocketSessionManager with private collection storage
90
+ # - NEW: Session key endpoint (/auth/websocket-session)
91
+ # - NEW: Session key integration in login flow
92
+ # - ENHANCED: WebSocket authentication with session key support
93
+ # - ENHANCED: CSRF middleware session key validation
94
+ # - ENHANCED: Multi-app WebSocket routing with session keys
95
+ # - BACKWARD COMPATIBLE: Cookie-based authentication fallback
96
+ # - UPDATED: All documentation for secure-by-default approach
97
+ # - COMPREHENSIVE: Unit and integration tests for session keys
94
98
  )
95
99
 
96
100
  __all__ = [
@@ -125,6 +125,12 @@ from .utils import (
125
125
  validate_password_strength_async,
126
126
  )
127
127
 
128
+ # WebSocket sessions
129
+ from .websocket_sessions import (
130
+ WebSocketSessionManager,
131
+ create_websocket_session_endpoint,
132
+ )
133
+
128
134
  __all__ = [
129
135
  # Base classes
130
136
  "BaseAuthorizationProvider",
@@ -232,4 +238,7 @@ __all__ = [
232
238
  "generate_csrf_token",
233
239
  "validate_csrf_token",
234
240
  "get_csrf_token",
241
+ # WebSocket sessions
242
+ "WebSocketSessionManager",
243
+ "create_websocket_session_endpoint",
235
244
  ]
mdb_engine/auth/csrf.py CHANGED
@@ -102,14 +102,16 @@ def validate_csrf_token(
102
102
  try:
103
103
  parts = token.split(":")
104
104
  if len(parts) != 3:
105
+ logger.debug("CSRF token has wrong format (expected 3 parts)")
105
106
  return False
106
107
 
107
108
  raw_token, timestamp_str, signature = parts
108
109
  timestamp = int(timestamp_str)
109
110
 
110
111
  # Check age
111
- if time.time() - timestamp > max_age:
112
- logger.debug("CSRF token expired")
112
+ age = time.time() - timestamp
113
+ if age > max_age:
114
+ logger.debug(f"CSRF token expired (age: {age:.0f}s, max: {max_age}s)")
113
115
  return False
114
116
 
115
117
  # Verify signature
@@ -119,7 +121,10 @@ def validate_csrf_token(
119
121
  ]
120
122
 
121
123
  if not hmac.compare_digest(signature, expected_sig):
122
- logger.warning("CSRF token signature mismatch")
124
+ logger.warning(
125
+ f"CSRF token signature mismatch. "
126
+ f"Token format: signed, Has secret: {bool(secret)}"
127
+ )
123
128
  return False
124
129
 
125
130
  return True
@@ -128,7 +133,10 @@ def validate_csrf_token(
128
133
  return False
129
134
 
130
135
  # Simple token validation (just check it exists and has reasonable length)
131
- return len(token) >= CSRF_TOKEN_LENGTH
136
+ is_valid = len(token) >= CSRF_TOKEN_LENGTH
137
+ if not is_valid:
138
+ logger.debug(f"CSRF token too short (length: {len(token)}, required: {CSRF_TOKEN_LENGTH})")
139
+ return is_valid
132
140
 
133
141
 
134
142
  class CSRFMiddleware(BaseHTTPMiddleware):
@@ -197,10 +205,116 @@ class CSRFMiddleware(BaseHTTPMiddleware):
197
205
  return True
198
206
  return False
199
207
 
208
+ def _websocket_requires_csrf(self, request: Request, path: str) -> bool:
209
+ """
210
+ Check if WebSocket endpoint requires CSRF validation.
211
+
212
+ Defaults to True (security by default). Can be disabled per-endpoint via manifest.json:
213
+ websockets.{endpoint}.auth.csrf_required = false
214
+
215
+ Args:
216
+ request: FastAPI request
217
+ path: WebSocket path (e.g., "/app-3/ws")
218
+
219
+ Returns:
220
+ True if CSRF validation is required, False otherwise
221
+ """
222
+ # Check parent app state for WebSocket configs
223
+ websocket_configs = getattr(request.app.state, "websocket_configs", None)
224
+ if not websocket_configs:
225
+ # No WebSocket configs found - use default (CSRF required for security by default)
226
+ return True
227
+
228
+ # Normalize path for matching
229
+ normalized_path = path.rstrip("/")
230
+
231
+ # Try to find matching app config
232
+ # WebSocket paths are registered as /app-slug/endpoint-path
233
+ # e.g., /app-3/ws where app_slug="app-3" and endpoint_path="/ws"
234
+ for app_slug, config in websocket_configs.items():
235
+ # Check each endpoint in this app's config
236
+ for endpoint_name, endpoint_config in config.items():
237
+ endpoint_path = endpoint_config.get("path", "")
238
+ # Normalize endpoint path
239
+ normalized_endpoint = endpoint_path.rstrip("/")
240
+
241
+ # Match patterns:
242
+ # 1. Full path match: /app-slug/endpoint-path
243
+ # 2. Endpoint-only match: /endpoint-path (if path starts with endpoint)
244
+ expected_full_path = f"/{app_slug}{normalized_endpoint}"
245
+ if (
246
+ normalized_path == expected_full_path
247
+ or normalized_path.endswith(normalized_endpoint)
248
+ or normalized_path == normalized_endpoint
249
+ ):
250
+ auth_config = endpoint_config.get("auth", {})
251
+ if isinstance(auth_config, dict):
252
+ # Return csrf_required setting (defaults to True - security by default)
253
+ csrf_required = auth_config.get("csrf_required", True)
254
+ logger.debug(
255
+ f"WebSocket {path} csrf_required={csrf_required} "
256
+ f"(from app={app_slug}, endpoint={endpoint_name})"
257
+ )
258
+ return csrf_required
259
+
260
+ # No matching config found - use default (CSRF required for security by default)
261
+ logger.debug(f"No WebSocket config match for {path}, using default csrf_required=true")
262
+ return True
263
+
200
264
  def _is_websocket_upgrade(self, request: Request) -> bool:
201
265
  """Check if request is a WebSocket upgrade request."""
202
266
  upgrade_header = request.headers.get("upgrade", "").lower()
203
- return upgrade_header == "websocket"
267
+ connection_header = request.headers.get("connection", "").lower()
268
+
269
+ # Primary check: WebSocket upgrade requires both Upgrade: websocket
270
+ # and Connection: Upgrade headers
271
+ has_upgrade_header = upgrade_header == "websocket"
272
+ has_connection_upgrade = "upgrade" in connection_header or "websocket" in connection_header
273
+
274
+ # Secondary check: If upgrade header is present but connection is
275
+ # overridden (e.g., by TestClient), check if path matches a known
276
+ # WebSocket route pattern
277
+ path_matches_websocket_route = False
278
+ if has_upgrade_header and not has_connection_upgrade:
279
+ # Check if path matches any configured WebSocket route
280
+ websocket_configs = getattr(request.app.state, "websocket_configs", None)
281
+ if websocket_configs:
282
+ path = request.url.path.rstrip("/") or "/"
283
+ for app_slug, config in websocket_configs.items():
284
+ for _endpoint_name, endpoint_config in config.items():
285
+ endpoint_path = endpoint_config.get("path", "").rstrip("/") or "/"
286
+ # Try various path matching patterns
287
+ expected_full_path = (
288
+ f"/{app_slug}{endpoint_path}"
289
+ if endpoint_path != "/"
290
+ else f"/{app_slug}"
291
+ )
292
+ # Match patterns:
293
+ # 1. Exact match with app prefix: /app-slug/endpoint-path
294
+ # 2. Endpoint-only match: /endpoint-path (if path ends with endpoint)
295
+ # 3. Root match: / matches / or /app-slug
296
+ if (
297
+ path == expected_full_path
298
+ or path.endswith(endpoint_path)
299
+ or path == endpoint_path
300
+ or (path == "/" and endpoint_path == "/")
301
+ or (path == f"/{app_slug}" and endpoint_path == "/")
302
+ ):
303
+ path_matches_websocket_route = True
304
+ break
305
+ if path_matches_websocket_route:
306
+ break
307
+
308
+ is_websocket = has_upgrade_header and (
309
+ has_connection_upgrade or path_matches_websocket_route
310
+ )
311
+ if is_websocket:
312
+ logger.debug(
313
+ f"WebSocket upgrade detected: path={request.url.path}, "
314
+ f"upgrade={upgrade_header}, connection={connection_header}, "
315
+ f"path_match={path_matches_websocket_route}"
316
+ )
317
+ return is_websocket
204
318
 
205
319
  def _get_allowed_origins(self, request: Request) -> list[str]:
206
320
  """
@@ -298,9 +412,22 @@ class CSRFMiddleware(BaseHTTPMiddleware):
298
412
  path = request.url.path
299
413
  method = request.method
300
414
 
415
+ # Debug: Log all requests to see what's happening
416
+ upgrade_header = request.headers.get("upgrade", "").lower()
417
+ connection_header = request.headers.get("connection", "").lower()
418
+ if upgrade_header or "websocket" in path.lower():
419
+ logger.info(
420
+ f"🔍 CSRF middleware: {method} {path}, "
421
+ f"upgrade={upgrade_header}, connection={connection_header}"
422
+ )
423
+
301
424
  # CRITICAL: Handle WebSocket upgrade requests BEFORE other CSRF checks
302
425
  # WebSocket upgrades use cookie-based authentication and require CSRF validation
303
426
  if self._is_websocket_upgrade(request):
427
+ logger.info(
428
+ f"🔌 CSRF middleware processing WebSocket upgrade: {path}, "
429
+ f"origin: {request.headers.get('origin')}"
430
+ )
304
431
  # Always validate origin for WebSocket connections (CSWSH protection)
305
432
  if not self._validate_websocket_origin(request):
306
433
  logger.warning(
@@ -320,49 +447,155 @@ class CSRFMiddleware(BaseHTTPMiddleware):
320
447
 
321
448
  auth_token_cookie = request.cookies.get(AUTH_COOKIE_NAME)
322
449
  if auth_token_cookie:
323
- # For WebSocket upgrades, CSRF protection relies on:
450
+ # SECURITY BY DEFAULT: WebSocket CSRF protection uses encrypted session keys
451
+ # stored in private collection via envelope encryption.
452
+ #
453
+ # Security Model:
324
454
  # 1. Origin validation (already done above) - primary defense
325
- # 2. SameSite cookies - prevents cross-site cookie sending
326
- # 3. CSRF cookie presence - ensures session is established
455
+ # 2. Encrypted session key validation - CSRF protection via database
456
+ # 3. SameSite cookies - prevents cross-site cookie sending
327
457
  #
328
- # Note: JavaScript WebSocket API cannot set custom headers,
329
- # so we cannot use double-submit cookie pattern (cookie + header).
330
- # Instead, we rely on Origin validation + SameSite cookies for CSRF protection.
331
- csrf_cookie_token = request.cookies.get(self.cookie_name)
332
- if not csrf_cookie_token:
333
- logger.warning(f"WebSocket upgrade missing CSRF cookie for {path}")
334
- return JSONResponse(
335
- status_code=status.HTTP_403_FORBIDDEN,
336
- content={"detail": "CSRF token missing for WebSocket authentication"},
458
+ # Session keys are:
459
+ # - Generated on authentication
460
+ # - Encrypted using envelope encryption (same as app secrets)
461
+ # - Stored in _mdb_engine_websocket_sessions private collection
462
+ # - Validated during WebSocket upgrade
463
+
464
+ # Check if this WebSocket endpoint requires CSRF validation
465
+ csrf_required = self._websocket_requires_csrf(request, path)
466
+
467
+ if csrf_required:
468
+ # Try to get WebSocket session manager from app state
469
+ websocket_session_manager = getattr(
470
+ request.app.state, "websocket_session_manager", None
337
471
  )
338
472
 
339
- # Validate CSRF token signature if secret is used
340
- if self.secret and not validate_csrf_token(
341
- csrf_cookie_token, self.secret, self.token_ttl
342
- ):
343
- logger.warning(f"WebSocket CSRF token validation failed for {path}")
344
- return JSONResponse(
345
- status_code=status.HTTP_403_FORBIDDEN,
346
- content={
347
- "detail": "CSRF token expired or invalid for WebSocket connection"
348
- },
473
+ if websocket_session_manager:
474
+ # Use encrypted session key validation (secure-by-default)
475
+ session_key = request.query_params.get(
476
+ "session_key"
477
+ ) or request.headers.get("X-WebSocket-Session-Key")
478
+
479
+ if not session_key:
480
+ logger.error(
481
+ f" WebSocket upgrade missing session key for {path}. "
482
+ f"Auth cookie present: {bool(auth_token_cookie)}. "
483
+ f"Tip: Generate session key via /auth/websocket-session endpoint."
484
+ )
485
+ return JSONResponse(
486
+ status_code=status.HTTP_403_FORBIDDEN,
487
+ content={
488
+ "detail": (
489
+ "WebSocket session key missing. "
490
+ "Generate session key via /auth/websocket-session endpoint."
491
+ )
492
+ },
493
+ )
494
+
495
+ # Validate session key against encrypted storage
496
+ try:
497
+ session_data = await websocket_session_manager.validate_session(
498
+ session_key
499
+ )
500
+ if not session_data:
501
+ logger.error(
502
+ f"❌ WebSocket session key validation failed for {path}. "
503
+ f"Session key: {session_key[:16]}..."
504
+ )
505
+ return JSONResponse(
506
+ status_code=status.HTTP_403_FORBIDDEN,
507
+ content={
508
+ "detail": (
509
+ "WebSocket session key expired or invalid. "
510
+ "Generate a new session key."
511
+ )
512
+ },
513
+ )
514
+
515
+ # Store session data in request state for WebSocket handler
516
+ request.state.websocket_session = session_data
517
+ logger.debug(
518
+ f"✅ WebSocket session key validated for {path} "
519
+ f"(user: {session_data.get('user_id')})"
520
+ )
521
+ except (
522
+ ValueError,
523
+ TypeError,
524
+ AttributeError,
525
+ RuntimeError,
526
+ ):
527
+ logger.exception("Error validating WebSocket session key")
528
+ return JSONResponse(
529
+ status_code=status.HTTP_403_FORBIDDEN,
530
+ content={"detail": "WebSocket session validation error"},
531
+ )
532
+ else:
533
+ # Fallback to cookie-based CSRF (backward compatibility)
534
+ csrf_cookie_token = request.cookies.get(self.cookie_name)
535
+ if not csrf_cookie_token:
536
+ logger.error(
537
+ f"❌ WebSocket upgrade missing CSRF cookie for {path}. "
538
+ f"Auth cookie present: {bool(auth_token_cookie)}, "
539
+ f"CSRF cookie name: {self.cookie_name}, "
540
+ f"Available cookies: {list(request.cookies.keys())}. "
541
+ f"Tip: Make a GET request first to receive CSRF cookie."
542
+ )
543
+ return JSONResponse(
544
+ status_code=status.HTTP_403_FORBIDDEN,
545
+ content={
546
+ "detail": (
547
+ "CSRF token missing for WebSocket authentication. "
548
+ "Make a GET request first to receive the CSRF cookie."
549
+ )
550
+ },
551
+ )
552
+
553
+ # Validate CSRF token signature if secret is used
554
+ if self.secret and not validate_csrf_token(
555
+ csrf_cookie_token, self.secret, self.token_ttl
556
+ ):
557
+ logger.error(f"❌ WebSocket CSRF token validation failed for {path}.")
558
+ return JSONResponse(
559
+ status_code=status.HTTP_403_FORBIDDEN,
560
+ content={
561
+ "detail": (
562
+ "CSRF token expired or invalid for WebSocket connection"
563
+ )
564
+ },
565
+ )
566
+
567
+ # If CSRF header is provided, validate it matches the cookie
568
+ # (Header is optional for WebSocket, but if present, must match cookie)
569
+ csrf_header_token = request.headers.get(self.header_name)
570
+ if csrf_header_token:
571
+ if not hmac.compare_digest(csrf_cookie_token, csrf_header_token):
572
+ logger.error(
573
+ f"❌ WebSocket CSRF header mismatch for {path}. "
574
+ f"Cookie token and header token do not match."
575
+ )
576
+ return JSONResponse(
577
+ status_code=status.HTTP_403_FORBIDDEN,
578
+ content={
579
+ "detail": (
580
+ "CSRF token mismatch: header token does not "
581
+ "match cookie token"
582
+ )
583
+ },
584
+ )
585
+ logger.debug(
586
+ f"✅ CSRF header validated and matches cookie for WebSocket {path}"
587
+ )
588
+
589
+ logger.debug(f"✅ CSRF cookie validation passed for WebSocket {path}")
590
+ else:
591
+ logger.debug(
592
+ f"✅ WebSocket CSRF validation skipped for {path} "
593
+ f"(csrf_required=false, Origin validation sufficient)"
349
594
  )
350
595
 
351
- # Optional: If CSRF header is provided, validate it matches cookie
352
- # (Some clients may send it, but it's not required for WebSocket upgrades)
353
- header_token = request.headers.get(self.header_name)
354
- if header_token:
355
- # If header is provided, validate it matches cookie (double-submit pattern)
356
- if not hmac.compare_digest(csrf_cookie_token, header_token):
357
- logger.warning(f"WebSocket CSRF token mismatch for {path}")
358
- return JSONResponse(
359
- status_code=status.HTTP_403_FORBIDDEN,
360
- content={"detail": "CSRF token invalid for WebSocket connection"},
361
- )
362
-
363
596
  logger.debug(
364
597
  f"WebSocket upgrade CSRF validation passed for {path} "
365
- f"(Origin validated, CSRF cookie present)"
598
+ f"(Origin validated, CSRF validated)"
366
599
  )
367
600
 
368
601
  # Origin validated (and CSRF validated if authenticated)
@@ -120,6 +120,7 @@ class SharedUserPool:
120
120
  token_expiry_hours: int = DEFAULT_TOKEN_EXPIRY_HOURS,
121
121
  allow_insecure_dev: bool = False,
122
122
  blacklist_fail_closed: bool = True,
123
+ websocket_session_manager: Any | None = None,
123
124
  ):
124
125
  """
125
126
  Initialize the shared user pool.
@@ -174,6 +175,7 @@ class SharedUserPool:
174
175
 
175
176
  self._token_expiry_hours = token_expiry_hours
176
177
  self._blacklist_indexes_created = False
178
+ self._websocket_session_manager = websocket_session_manager
177
179
 
178
180
  logger.info(f"SharedUserPool initialized (algorithm={jwt_algorithm})")
179
181
 
@@ -340,7 +342,9 @@ class SharedUserPool:
340
342
  ip_address: str | None = None,
341
343
  fingerprint: str | None = None,
342
344
  session_binding: dict[str, Any] | None = None,
343
- ) -> str | None:
345
+ generate_websocket_session: bool = True,
346
+ app_slug: str | None = None,
347
+ ) -> str | tuple[str, str] | None:
344
348
  """
345
349
  Authenticate user and return JWT token.
346
350
 
@@ -352,9 +356,14 @@ class SharedUserPool:
352
356
  session_binding: Session binding config from manifest:
353
357
  - bind_ip: Include IP in token claims
354
358
  - bind_fingerprint: Include fingerprint in token claims
359
+ generate_websocket_session: If True and WebSocket session manager available,
360
+ also generate WebSocket session key (default: True)
361
+ app_slug: Optional app slug for WebSocket session scoping
355
362
 
356
363
  Returns:
357
- JWT token if authentication succeeds, None otherwise
364
+ JWT token if authentication succeeds, None otherwise.
365
+ If generate_websocket_session=True and session manager available,
366
+ returns tuple (jwt_token, websocket_session_key), otherwise just jwt_token.
358
367
  """
359
368
  user = await self._collection.find_one(
360
369
  {
@@ -392,7 +401,28 @@ class SharedUserPool:
392
401
  # Generate JWT token with session binding claims
393
402
  token = self._generate_token(user, extra_claims=extra_claims or None)
394
403
 
404
+ # Generate WebSocket session key if requested and manager available
405
+ websocket_session_key = None
406
+ if generate_websocket_session and self._websocket_session_manager:
407
+ try:
408
+ user_id = str(user["_id"])
409
+ websocket_session_key = await self._websocket_session_manager.create_session(
410
+ user_id=user_id,
411
+ user_email=email,
412
+ app_slug=app_slug,
413
+ )
414
+ logger.debug(
415
+ f"Generated WebSocket session key for user '{email}' " f"(app: {app_slug})"
416
+ )
417
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
418
+ # Log but don't fail authentication if WebSocket session generation fails
419
+ logger.warning(f"Failed to generate WebSocket session key: {e}")
420
+
395
421
  logger.info(f"User '{email}' authenticated successfully")
422
+
423
+ # Return tuple if WebSocket session key was generated, otherwise just token
424
+ if websocket_session_key:
425
+ return (token, websocket_session_key)
396
426
  return token
397
427
 
398
428
  async def validate_token(self, token: str) -> dict[str, Any] | None:
mdb_engine/auth/utils.py CHANGED
@@ -514,16 +514,41 @@ async def login_user(
514
514
  ip_address=device_info.get("ip_address"),
515
515
  )
516
516
 
517
+ # Generate WebSocket session key if WebSocket session manager available
518
+ websocket_session_key = None
519
+ try:
520
+ # Try to get WebSocket session manager from app state
521
+ app = getattr(request, "app", None)
522
+ if app:
523
+ websocket_session_manager = getattr(app.state, "websocket_session_manager", None)
524
+ if websocket_session_manager:
525
+ # Get app slug from request state if available
526
+ app_slug = getattr(request.state, "app_slug", None)
527
+ websocket_session_key = await websocket_session_manager.create_session(
528
+ user_id=str(user["_id"]),
529
+ user_email=user["email"],
530
+ app_slug=app_slug,
531
+ )
532
+ logger.debug(
533
+ f"Generated WebSocket session key for user '{user['email']}' "
534
+ f"(app: {app_slug})"
535
+ )
536
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
537
+ # Log but don't fail login if WebSocket session generation fails
538
+ logger.warning(f"Failed to generate WebSocket session key during login: {e}")
539
+
517
540
  # Create response
541
+ response_data = {
542
+ "success": True,
543
+ "user": {"email": user["email"], "user_id": str(user["_id"])},
544
+ }
545
+ if websocket_session_key:
546
+ response_data["websocket_session_key"] = websocket_session_key
547
+
518
548
  if redirect_url:
519
549
  response = RedirectResponse(url=redirect_url, status_code=302)
520
550
  else:
521
- response = JSONResponse(
522
- {
523
- "success": True,
524
- "user": {"email": user["email"], "user_id": str(user["_id"])},
525
- }
526
- )
551
+ response = JSONResponse(response_data)
527
552
 
528
553
  # Set cookies
529
554
  set_auth_cookies(
@@ -0,0 +1,433 @@
1
+ """
2
+ WebSocket Session Manager with Envelope Encryption
3
+
4
+ Manages WebSocket session keys using envelope encryption and private collections.
5
+ Provides secure-by-default WebSocket authentication without relying on CSRF cookies.
6
+
7
+ This module is part of MDB_ENGINE - MongoDB Engine.
8
+
9
+ Security Model:
10
+ - Session keys generated on authentication
11
+ - Stored encrypted in _mdb_engine_websocket_sessions collection
12
+ - Validated during WebSocket upgrade
13
+ - Uses envelope encryption (same as app secrets)
14
+ - Security by default: CSRF always required
15
+ """
16
+
17
+ import base64
18
+ import logging
19
+ import secrets
20
+ from collections.abc import Callable
21
+ from datetime import datetime, timedelta
22
+ from typing import Any
23
+
24
+ from motor.motor_asyncio import AsyncIOMotorDatabase
25
+ from pymongo.errors import OperationFailure, PyMongoError
26
+
27
+ from ..core.encryption import EnvelopeEncryptionService
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Collection name for storing encrypted WebSocket session keys
32
+ WEBSOCKET_SESSIONS_COLLECTION_NAME = "_mdb_engine_websocket_sessions"
33
+
34
+ # Session key configuration
35
+ SESSION_KEY_SIZE = 32 # 256 bits
36
+ SESSION_TTL_HOURS = 24 # Sessions expire after 24 hours
37
+
38
+
39
+ class WebSocketSessionManager:
40
+ """
41
+ Manages WebSocket session keys using envelope encryption.
42
+
43
+ Session keys are:
44
+ - Generated on user authentication
45
+ - Encrypted using envelope encryption
46
+ - Stored in private collection (_mdb_engine_websocket_sessions)
47
+ - Validated during WebSocket upgrade
48
+ - Automatically expired after TTL
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ mongo_db: AsyncIOMotorDatabase,
54
+ encryption_service: EnvelopeEncryptionService,
55
+ ):
56
+ """
57
+ Initialize the WebSocket session manager.
58
+
59
+ Args:
60
+ mongo_db: MongoDB database instance (raw, not scoped)
61
+ encryption_service: Envelope encryption service instance
62
+ """
63
+ self._mongo_db = mongo_db
64
+ self._encryption_service = encryption_service
65
+ self._sessions_collection = mongo_db[WEBSOCKET_SESSIONS_COLLECTION_NAME]
66
+
67
+ @staticmethod
68
+ def generate_session_key() -> str:
69
+ """
70
+ Generate a random WebSocket session key.
71
+
72
+ Returns:
73
+ Base64-encoded session key string
74
+ """
75
+ key_bytes = secrets.token_bytes(SESSION_KEY_SIZE)
76
+ return base64.urlsafe_b64encode(key_bytes).decode().rstrip("=")
77
+
78
+ async def create_session(
79
+ self,
80
+ user_id: str,
81
+ user_email: str | None = None,
82
+ app_slug: str | None = None,
83
+ ) -> str:
84
+ """
85
+ Create a new WebSocket session with encrypted session key.
86
+
87
+ Args:
88
+ user_id: User ID
89
+ user_email: Optional user email
90
+ app_slug: Optional app slug for scoping
91
+
92
+ Returns:
93
+ Plaintext session key (to be sent to client)
94
+
95
+ Raises:
96
+ OperationFailure: If MongoDB operation fails
97
+ """
98
+ try:
99
+ # Generate session key
100
+ session_key = self.generate_session_key()
101
+
102
+ # Encrypt session key using envelope encryption
103
+ encrypted_key, encrypted_dek = self._encryption_service.encrypt_secret(session_key)
104
+
105
+ # Encode as base64 for storage
106
+ encrypted_key_b64 = base64.b64encode(encrypted_key).decode()
107
+ encrypted_dek_b64 = base64.b64encode(encrypted_dek).decode()
108
+
109
+ # Calculate expiration
110
+ expires_at = datetime.utcnow() + timedelta(hours=SESSION_TTL_HOURS)
111
+
112
+ # Prepare document
113
+ document = {
114
+ "_id": session_key, # Use session key as ID for fast lookup
115
+ "user_id": user_id,
116
+ "user_email": user_email,
117
+ "app_slug": app_slug,
118
+ "encrypted_key": encrypted_key_b64,
119
+ "encrypted_dek": encrypted_dek_b64,
120
+ "algorithm": "AES-256-GCM",
121
+ "created_at": datetime.utcnow(),
122
+ "expires_at": expires_at,
123
+ }
124
+
125
+ # Store in private collection
126
+ await self._sessions_collection.insert_one(document)
127
+
128
+ logger.info(
129
+ f"Created WebSocket session for user '{user_id}' "
130
+ f"(app: {app_slug}, expires: {expires_at})"
131
+ )
132
+
133
+ return session_key
134
+
135
+ except (OperationFailure, PyMongoError):
136
+ logger.exception("Failed to create WebSocket session")
137
+ raise
138
+
139
+ async def validate_session(
140
+ self,
141
+ session_key: str,
142
+ user_id: str | None = None,
143
+ ) -> dict[str, Any] | None:
144
+ """
145
+ Validate a WebSocket session key.
146
+
147
+ Args:
148
+ session_key: Session key to validate
149
+ user_id: Optional user ID for additional validation
150
+
151
+ Returns:
152
+ Session document if valid, None otherwise
153
+
154
+ Raises:
155
+ OperationFailure: If MongoDB operation fails
156
+ """
157
+ try:
158
+ # Find session by key
159
+ session_doc = await self._sessions_collection.find_one({"_id": session_key})
160
+
161
+ if not session_doc:
162
+ logger.warning(f"WebSocket session not found: {session_key[:16]}...")
163
+ return None
164
+
165
+ # Check expiration
166
+ expires_at = session_doc.get("expires_at")
167
+ if expires_at and expires_at < datetime.utcnow():
168
+ logger.warning(
169
+ f"WebSocket session expired: {session_key[:16]}... " f"(expired: {expires_at})"
170
+ )
171
+ # Clean up expired session
172
+ await self._sessions_collection.delete_one({"_id": session_key})
173
+ return None
174
+
175
+ # Optional: Validate user_id matches
176
+ if user_id and session_doc.get("user_id") != user_id:
177
+ logger.warning(
178
+ f"WebSocket session user mismatch: "
179
+ f"session_user={session_doc.get('user_id')}, "
180
+ f"provided_user={user_id}"
181
+ )
182
+ return None
183
+
184
+ # Decrypt session key to verify it's valid
185
+ try:
186
+ encrypted_key = base64.b64decode(session_doc["encrypted_key"])
187
+ encrypted_dek = base64.b64decode(session_doc["encrypted_dek"])
188
+ decrypted_key = self._encryption_service.decrypt_secret(
189
+ encrypted_key, encrypted_dek
190
+ )
191
+
192
+ # Verify decrypted key matches session_key
193
+ if decrypted_key != session_key:
194
+ logger.error(
195
+ f"WebSocket session key decryption mismatch: "
196
+ f"session_key={session_key[:16]}..."
197
+ )
198
+ return None
199
+
200
+ except (ValueError, TypeError, AttributeError, KeyError):
201
+ logger.exception("Failed to decrypt WebSocket session key")
202
+ return None
203
+
204
+ logger.debug(
205
+ f"Validated WebSocket session for user '{session_doc.get('user_id')}' "
206
+ f"(app: {session_doc.get('app_slug')})"
207
+ )
208
+
209
+ return {
210
+ "user_id": session_doc.get("user_id"),
211
+ "user_email": session_doc.get("user_email"),
212
+ "app_slug": session_doc.get("app_slug"),
213
+ "created_at": session_doc.get("created_at"),
214
+ "expires_at": session_doc.get("expires_at"),
215
+ }
216
+
217
+ except (OperationFailure, PyMongoError):
218
+ logger.exception("Failed to validate WebSocket session")
219
+ raise
220
+
221
+ async def revoke_session(self, session_key: str) -> bool:
222
+ """
223
+ Revoke a WebSocket session.
224
+
225
+ Args:
226
+ session_key: Session key to revoke
227
+
228
+ Returns:
229
+ True if session was revoked, False if not found
230
+ """
231
+ try:
232
+ result = await self._sessions_collection.delete_one({"_id": session_key})
233
+ if result.deleted_count > 0:
234
+ logger.info(f"Revoked WebSocket session: {session_key[:16]}...")
235
+ return True
236
+ return False
237
+ except (OperationFailure, PyMongoError):
238
+ logger.exception("Failed to revoke WebSocket session")
239
+ return False
240
+
241
+ async def revoke_user_sessions(self, user_id: str, app_slug: str | None = None) -> int:
242
+ """
243
+ Revoke all sessions for a user.
244
+
245
+ Args:
246
+ user_id: User ID
247
+ app_slug: Optional app slug filter
248
+
249
+ Returns:
250
+ Number of sessions revoked
251
+ """
252
+ try:
253
+ query = {"user_id": user_id}
254
+ if app_slug:
255
+ query["app_slug"] = app_slug
256
+
257
+ result = await self._sessions_collection.delete_many(query)
258
+ logger.info(
259
+ f"Revoked {result.deleted_count} WebSocket sessions "
260
+ f"for user '{user_id}' (app: {app_slug})"
261
+ )
262
+ return result.deleted_count
263
+ except (OperationFailure, PyMongoError):
264
+ logger.exception("Failed to revoke user WebSocket sessions")
265
+ return 0
266
+
267
+ async def cleanup_expired_sessions(self) -> int:
268
+ """
269
+ Clean up expired WebSocket sessions.
270
+
271
+ Returns:
272
+ Number of sessions cleaned up
273
+ """
274
+ try:
275
+ result = await self._sessions_collection.delete_many(
276
+ {"expires_at": {"$lt": datetime.utcnow()}}
277
+ )
278
+ if result.deleted_count > 0:
279
+ logger.info(f"Cleaned up {result.deleted_count} expired WebSocket sessions")
280
+ return result.deleted_count
281
+ except (OperationFailure, PyMongoError):
282
+ logger.exception("Failed to cleanup expired WebSocket sessions")
283
+ return 0
284
+
285
+
286
+ def create_websocket_session_endpoint(
287
+ session_manager: WebSocketSessionManager,
288
+ ) -> Callable:
289
+ """
290
+ Create a FastAPI endpoint for generating WebSocket session keys.
291
+
292
+ This endpoint requires authentication and generates a new WebSocket session key
293
+ for the authenticated user. The session key is encrypted and stored in the
294
+ private collection.
295
+
296
+ Args:
297
+ session_manager: WebSocketSessionManager instance
298
+
299
+ Returns:
300
+ FastAPI route handler function
301
+
302
+ Example:
303
+ ```python
304
+ from mdb_engine.auth.websocket_sessions import (
305
+ WebSocketSessionManager,
306
+ create_websocket_session_endpoint,
307
+ )
308
+ from mdb_engine.core.encryption import EnvelopeEncryptionService
309
+
310
+ # Initialize session manager
311
+ encryption_service = EnvelopeEncryptionService()
312
+ session_manager = WebSocketSessionManager(
313
+ mongo_db=db,
314
+ encryption_service=encryption_service,
315
+ )
316
+
317
+ # Create endpoint
318
+ endpoint = create_websocket_session_endpoint(session_manager)
319
+ app.get("/auth/websocket-session")(endpoint)
320
+ ```
321
+
322
+ The endpoint:
323
+ - Requires authentication (user must be logged in)
324
+ - Returns JSON: `{"session_key": "...", "expires_at": "..."}`
325
+ - Uses user info from `request.state.user` (set by SharedAuthMiddleware)
326
+ """
327
+ from fastapi import Request, status
328
+ from fastapi.responses import JSONResponse
329
+
330
+ async def websocket_session_endpoint(request: Request) -> JSONResponse:
331
+ """
332
+ Generate a WebSocket session key for the authenticated user.
333
+
334
+ Requires:
335
+ - User to be authenticated (via request.state.user or auth cookie)
336
+ - WebSocket session manager to be available
337
+
338
+ Returns:
339
+ - JSONResponse with session_key and expires_at
340
+ """
341
+ # Check if user is authenticated (set by middleware)
342
+ user = getattr(request.state, "user", None)
343
+
344
+ # If not set by middleware, try to authenticate using cookie
345
+ # This handles the case where endpoint is on parent app without auth middleware
346
+ if not user:
347
+ from .shared_middleware import AUTH_COOKIE_NAME
348
+
349
+ # Get user pool from app state
350
+ user_pool = None
351
+ try:
352
+ if hasattr(request, "app") and hasattr(request.app, "state"):
353
+ user_pool = getattr(request.app.state, "user_pool", None)
354
+ except (AttributeError, TypeError):
355
+ pass
356
+
357
+ # Only try to authenticate if we have a real user pool (not None)
358
+ if user_pool is not None:
359
+ # Extract token from cookie
360
+ token = None
361
+ try:
362
+ if hasattr(request, "cookies"):
363
+ token = request.cookies.get(AUTH_COOKIE_NAME)
364
+ except (AttributeError, TypeError):
365
+ pass
366
+
367
+ if token:
368
+ try:
369
+ # Validate token and get user
370
+ user = await user_pool.validate_token(token)
371
+ except (TypeError, AttributeError):
372
+ # If user_pool is a mock that can't be awaited, ignore
373
+ pass
374
+
375
+ if not user:
376
+ return JSONResponse(
377
+ status_code=status.HTTP_401_UNAUTHORIZED,
378
+ content={"detail": "Authentication required"},
379
+ )
380
+
381
+ # Extract user info
382
+ # Prefer user_id, sub (JWT standard), or _id (MongoDB document ID)
383
+ user_id = user.get("user_id") or user.get("sub") or user.get("_id")
384
+ if not user_id:
385
+ # Email is not a valid user_id - it's just metadata
386
+ logger.error("Cannot generate WebSocket session: user_id not found in user data")
387
+ return JSONResponse(
388
+ status_code=status.HTTP_400_BAD_REQUEST,
389
+ content={"detail": "Invalid user data"},
390
+ )
391
+ user_email = user.get("email")
392
+ app_slug = getattr(request.state, "app_slug", None)
393
+
394
+ try:
395
+ # Generate session key
396
+ session_key = await session_manager.create_session(
397
+ user_id=str(user_id),
398
+ user_email=user_email,
399
+ app_slug=app_slug,
400
+ )
401
+
402
+ # Get expiration time (24 hours from now)
403
+ from datetime import datetime, timedelta
404
+
405
+ expires_at = datetime.utcnow() + timedelta(hours=SESSION_TTL_HOURS)
406
+
407
+ logger.info(
408
+ f"Generated WebSocket session key for user '{user_id}' " f"(app: {app_slug})"
409
+ )
410
+
411
+ return JSONResponse(
412
+ {
413
+ "session_key": session_key,
414
+ "expires_at": expires_at.isoformat(),
415
+ "ttl_hours": SESSION_TTL_HOURS,
416
+ }
417
+ )
418
+
419
+ except (
420
+ ValueError,
421
+ TypeError,
422
+ AttributeError,
423
+ RuntimeError,
424
+ OperationFailure,
425
+ PyMongoError,
426
+ ):
427
+ logger.exception("Failed to generate WebSocket session key")
428
+ return JSONResponse(
429
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
430
+ content={"detail": "Failed to generate WebSocket session key"},
431
+ )
432
+
433
+ return websocket_session_endpoint
mdb_engine/core/engine.py CHANGED
@@ -148,6 +148,7 @@ class MongoDBEngine:
148
148
  self._service_initializer: ServiceInitializer | None = None
149
149
  self._encryption_service: EnvelopeEncryptionService | None = None
150
150
  self._app_secrets_manager: AppSecretsManager | None = None
151
+ self._websocket_session_manager: Any | None = None # WebSocketSessionManager
151
152
 
152
153
  # Store app read_scopes mapping for validation
153
154
  self._app_read_scopes: dict[str, list[str]] = {}
@@ -201,6 +202,13 @@ class MongoDBEngine:
201
202
  mongo_db=self._connection_manager.mongo_db,
202
203
  encryption_service=self._encryption_service,
203
204
  )
205
+ # Initialize WebSocket session manager for secure-by-default WebSocket auth
206
+ from ..auth.websocket_sessions import WebSocketSessionManager
207
+
208
+ self._websocket_session_manager = WebSocketSessionManager(
209
+ mongo_db=self._connection_manager.mongo_db,
210
+ encryption_service=self._encryption_service,
211
+ )
204
212
 
205
213
  # Set up component managers
206
214
  self._app_registration_manager = AppRegistrationManager(
@@ -2283,6 +2291,11 @@ class MongoDBEngine:
2283
2291
  logger.debug(f"No WebSocket configuration found for app '{slug}'")
2284
2292
  return
2285
2293
 
2294
+ # Store WebSocket config in parent app state for CSRF middleware to access
2295
+ if not hasattr(parent_app.state, "websocket_configs"):
2296
+ parent_app.state.websocket_configs = {}
2297
+ parent_app.state.websocket_configs[slug] = websockets_config
2298
+
2286
2299
  try:
2287
2300
  from fastapi import APIRouter
2288
2301
 
@@ -2489,6 +2502,13 @@ class MongoDBEngine:
2489
2502
  child_app.state.audit_log = app.state.audit_log
2490
2503
  logger.debug(f"Shared user_pool with child app '{slug}'")
2491
2504
 
2505
+ # Share WebSocket session manager with child app
2506
+ if hasattr(app.state, "websocket_session_manager"):
2507
+ child_app.state.websocket_session_manager = (
2508
+ app.state.websocket_session_manager
2509
+ )
2510
+ logger.debug(f"Shared WebSocket session manager with child app '{slug}'")
2511
+
2492
2512
  # Add middleware for app context helpers
2493
2513
  from starlette.middleware.base import BaseHTTPMiddleware
2494
2514
  from starlette.requests import Request
@@ -2836,12 +2856,31 @@ class MongoDBEngine:
2836
2856
  # Create CSRF middleware with default config (will use parent app's CORS config)
2837
2857
  # Exempt routes that don't need CSRF (health checks, public routes from child apps)
2838
2858
  # all_public_routes includes base routes + child app public routes with path prefixes
2859
+ # Add WebSocket session endpoint to public routes (it handles its own auth)
2860
+ public_routes_with_session_endpoint = list(all_public_routes) + [
2861
+ "/auth/websocket-session"
2862
+ ]
2839
2863
  parent_csrf_config = {
2840
2864
  "csrf_protection": True,
2841
- "public_routes": all_public_routes,
2865
+ "public_routes": public_routes_with_session_endpoint,
2842
2866
  }
2843
2867
  csrf_middleware = create_csrf_middleware(parent_csrf_config)
2844
2868
  parent_app.add_middleware(csrf_middleware)
2869
+
2870
+ # Store WebSocket session manager in app state for CSRF middleware and endpoints
2871
+ if self._websocket_session_manager:
2872
+ parent_app.state.websocket_session_manager = self._websocket_session_manager
2873
+ logger.info("WebSocket session manager stored in parent app state")
2874
+
2875
+ # Register WebSocket session endpoint on parent app
2876
+ from ..auth.websocket_sessions import create_websocket_session_endpoint
2877
+
2878
+ session_endpoint = create_websocket_session_endpoint(
2879
+ self._websocket_session_manager
2880
+ )
2881
+ parent_app.get("/auth/websocket-session")(session_endpoint)
2882
+ logger.info("WebSocket session endpoint registered at /auth/websocket-session")
2883
+
2845
2884
  logger.info("CSRFMiddleware added to parent app for WebSocket origin validation")
2846
2885
 
2847
2886
  # Add shared CORS middleware if configured
@@ -3320,6 +3359,7 @@ class MongoDBEngine:
3320
3359
  self._shared_user_pool = SharedUserPool(
3321
3360
  self._connection_manager.mongo_db,
3322
3361
  allow_insecure_dev=is_dev,
3362
+ websocket_session_manager=self._websocket_session_manager,
3323
3363
  )
3324
3364
  await self._shared_user_pool.ensure_indexes()
3325
3365
  logger.info("SharedUserPool initialized")
@@ -1338,6 +1338,18 @@ MANIFEST_SCHEMA_V2 = {
1338
1338
  "auth is required (default: false)"
1339
1339
  ),
1340
1340
  },
1341
+ "csrf_required": {
1342
+ "type": "boolean",
1343
+ "default": True,
1344
+ "description": (
1345
+ "Require CSRF validation for WebSocket connections "
1346
+ "(default: true - security by default). "
1347
+ "When true, uses encrypted session keys stored in "
1348
+ "private collection for CSRF protection. "
1349
+ "Set to false to use Origin validation + "
1350
+ "SameSite cookies only."
1351
+ ),
1352
+ },
1341
1353
  },
1342
1354
  "additionalProperties": False,
1343
1355
  "description": (
mdb_engine/core/types.py CHANGED
@@ -243,6 +243,7 @@ class WebSocketAuthDict(TypedDict, total=False):
243
243
 
244
244
  required: bool
245
245
  allow_anonymous: bool
246
+ csrf_required: bool # Whether CSRF cookie is required (default: False)
246
247
 
247
248
 
248
249
  class WebSocketEndpointDict(TypedDict, total=False):
@@ -365,12 +365,11 @@ async def authenticate_websocket(
365
365
  require_auth: bool = True,
366
366
  ) -> tuple[str | None, str | None]:
367
367
  """
368
- Authenticate a WebSocket connection via httpOnly cookies.
368
+ Authenticate a WebSocket connection via session key or httpOnly cookies.
369
369
 
370
- Uses cookie-based authentication with CSRF protection:
371
- - Token stored in httpOnly cookie (not accessible to JavaScript)
372
- - CSRF token validated via double-submit cookie pattern
373
- - Origin validation provides additional protection
370
+ Authentication methods (in order of preference):
371
+ 1. Session key (query param or header) - secure-by-default, uses envelope encryption
372
+ 2. Cookie-based authentication - backward compatibility fallback
374
373
 
375
374
  Args:
376
375
  websocket: FastAPI WebSocket instance (can access headers before accept)
@@ -394,8 +393,51 @@ async def authenticate_websocket(
394
393
  return None, None
395
394
 
396
395
  try:
397
- # Extract token from httpOnly cookie
398
- # Use same cookie name as SharedAuthMiddleware for consistency
396
+ # Try to get WebSocket session manager from app
397
+ websocket_session_manager = None
398
+ try:
399
+ app = getattr(websocket, "app", None)
400
+ if app:
401
+ websocket_session_manager = getattr(app.state, "websocket_session_manager", None)
402
+ except (AttributeError, TypeError):
403
+ pass
404
+
405
+ # Method 1: Try session key authentication (secure-by-default)
406
+ session_key = None
407
+ try:
408
+ # Check query params first
409
+ if hasattr(websocket, "query_params"):
410
+ session_key = websocket.query_params.get("session_key")
411
+
412
+ # Check headers if not in query params
413
+ if not session_key and hasattr(websocket, "headers"):
414
+ session_key = websocket.headers.get("X-WebSocket-Session-Key")
415
+ except (AttributeError, TypeError, KeyError):
416
+ pass
417
+
418
+ if session_key and websocket_session_manager:
419
+ try:
420
+ # Validate session key
421
+ session_data = await websocket_session_manager.validate_session(session_key)
422
+ if session_data:
423
+ user_id = session_data.get("user_id")
424
+ user_email = session_data.get("user_email")
425
+
426
+ logger.info(
427
+ f"WebSocket authenticated successfully for app '{app_slug}': {user_email} "
428
+ f"(method: session_key)"
429
+ )
430
+ return user_id, user_email
431
+ else:
432
+ logger.warning(
433
+ f"WebSocket session key validation failed for app '{app_slug}'. "
434
+ f"Session key: {session_key[:16]}..."
435
+ )
436
+ except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
437
+ logger.warning(f"WebSocket session key validation error for app '{app_slug}': {e}")
438
+ # Fall through to cookie-based auth
439
+
440
+ # Method 2: Fall back to cookie-based authentication (backward compatibility)
399
441
  from ..auth.shared_middleware import AUTH_COOKIE_NAME
400
442
 
401
443
  cookies = _get_cookies_from_websocket(websocket)
@@ -403,17 +445,19 @@ async def authenticate_websocket(
403
445
 
404
446
  if not token:
405
447
  logger.error(
406
- f"❌ No token cookie found for WebSocket connection to app '{app_slug}' "
448
+ f"❌ No authentication found for WebSocket connection to app '{app_slug}' "
407
449
  f"(require_auth={require_auth}). "
450
+ f"Session key: {bool(session_key)}, Cookie: {bool(token)}, "
408
451
  f"Available cookies: {list(cookies.keys()) if cookies else 'none'}. "
409
- f"Ensure httpOnly cookie is set during authentication."
452
+ f"Ensure session key or httpOnly cookie is set during authentication."
410
453
  )
411
454
  if require_auth:
412
455
  return None, None # Signal auth failure
413
456
  return None, None
414
457
 
415
458
  logger.info(
416
- f"WebSocket token found in cookie for app '{app_slug}' " "(cookie-based authentication)"
459
+ f"WebSocket token found in cookie for app '{app_slug}' "
460
+ "(cookie-based authentication, fallback)"
417
461
  )
418
462
 
419
463
  # Decode and validate token
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -1,12 +1,12 @@
1
1
  mdb_engine/README.md,sha256=T3EFGcPopY9LslYW3lxgG3hohWkAOmBNbYG0FDMUJiY,3502
2
- mdb_engine/__init__.py,sha256=Uz23q_g7sJjuMs5TzIZbS34wMO0oAtS1SWa6cqBdhqw,3705
2
+ mdb_engine/__init__.py,sha256=vishQ3BF6oGQOmAsrMG1UJ-5C-3FNM49JSHC35MhH0k,3937
3
3
  mdb_engine/config.py,sha256=DTAyxfKB8ogyI0v5QR9Y-SJOgXQr_eDBCKxNBSqEyLc,7269
4
4
  mdb_engine/constants.py,sha256=eaotvW57TVOg7rRbLziGrVNoP7adgw_G9iVByHezc_A,7837
5
5
  mdb_engine/dependencies.py,sha256=MJuYQhZ9ZGzXlip1ha5zba9Rvn04HDPWahJFJH81Q2s,14107
6
6
  mdb_engine/exceptions.py,sha256=NkBSdTBNP9bVTtI6w6mAoEfeVZDq-Bg7vCF2L26ZDZo,8442
7
7
  mdb_engine/auth/ARCHITECTURE.md,sha256=JXZsjEIpNz4Szk18KaOiEabhEuz1pmYWXQRN-EJEHDM,4076
8
8
  mdb_engine/auth/README.md,sha256=IVlUOat96V3yM6wk4T-u4GxJ4-WF05eBSJBlvIexyBo,37285
9
- mdb_engine/auth/__init__.py,sha256=QemWqAVvqlxjF-UbzMTGP5AUOeuaAsUEtK4PMxmozmA,5852
9
+ mdb_engine/auth/__init__.py,sha256=SWyMZZvCx2u1IXMlOBbw2h1CHAhIBPuwMQ6iDzlqMsQ,6075
10
10
  mdb_engine/auth/audit.py,sha256=UQ0Bj5Zxp5et9A21YZCVkUjaABFqO1CXaMrWxNcdxm8,17352
11
11
  mdb_engine/auth/base.py,sha256=4E3XkbruZLG9lc6aC56w0ypjnfiR5RbtjQKwn4am8to,7418
12
12
  mdb_engine/auth/casbin_factory.py,sha256=oZLHIPyVA1hiho__ledRVDoBisaZgo96WZIKa0-kQic,16610
@@ -14,7 +14,7 @@ mdb_engine/auth/casbin_models.py,sha256=7XtFmRBhhjw1nKprnluvjyJoTj5fzdPeQwVvo6fI
14
14
  mdb_engine/auth/config_defaults.py,sha256=1YI_hIHuTiEXpkEYMcufNHdLr1oxPiJylg3CKrJCSGY,2012
15
15
  mdb_engine/auth/config_helpers.py,sha256=Qharb2YagLOKDGtE7XhYRDbBoQ_KGykrcIKrsOwWIJ4,6303
16
16
  mdb_engine/auth/cookie_utils.py,sha256=glsSocSmy-_wRTLro0xy17s84oBk3HPDPL-FVXl7Rv8,5302
17
- mdb_engine/auth/csrf.py,sha256=O6q7BOZuzUy6N71EUuabhBqYsRYHuAcCr5Fyabcy1vw,20538
17
+ mdb_engine/auth/csrf.py,sha256=s3OfZjLSIaABmLORbAGrTfVp4dWEbP7l9T_lf2bNEi0,32538
18
18
  mdb_engine/auth/decorators.py,sha256=LkVVEuRrT0Iz8EwctN14BEi3fSV-xtN6DaGXgtbiYYo,12287
19
19
  mdb_engine/auth/dependencies.py,sha256=JB1iYvZJgTR6gcaiGe_GJFCS6NdUKMxWBZRv6vVxnzw,27112
20
20
  mdb_engine/auth/helpers.py,sha256=BCrid985cYh-3h5ZMUV9TES0q40uJXio4oYKQZta7KA,1970
@@ -27,11 +27,12 @@ mdb_engine/auth/rate_limiter.py,sha256=l3EYZE1Kz9yVfZwNrKq_1AgdD7GXB1WOLSqqGQVSS
27
27
  mdb_engine/auth/restrictions.py,sha256=tOyQBO_w0bK9zmTsOPZf9cbvh4oITvpNfSxIXt-XrcU,8824
28
28
  mdb_engine/auth/session_manager.py,sha256=ywWJjTarm-obgJ3zO3s-1cdqEYe0XrozlY00q_yMJ8I,15396
29
29
  mdb_engine/auth/shared_middleware.py,sha256=0iSbRkwdivL1NIj7Gr161qPJiqcw0JafOpZLCkXjT7k,37633
30
- mdb_engine/auth/shared_users.py,sha256=KTc4D9zRaYaIVto7PqyWd5RT4J97cp6AnJ5i_PR_7eg,27775
30
+ mdb_engine/auth/shared_users.py,sha256=huE7e3ywqA7fZ2HYIPNc-kQ9syEdpYUrCYjYbX3bVhA,29456
31
31
  mdb_engine/auth/token_lifecycle.py,sha256=Q9S1X2Y6W7Ckt5PvyYXswBRh2Tg9DGpyRv_3Xve7VYQ,6708
32
32
  mdb_engine/auth/token_store.py,sha256=-B8j5RH5YEoKsswF4rnMoI51BaxMe4icke3kuehXmcI,9121
33
33
  mdb_engine/auth/users.py,sha256=t9Us2_A_wKOL9qy1O_SBwTvapAyNztn0v8padxJVq6A,49891
34
- mdb_engine/auth/utils.py,sha256=g7fXkxIb8yuvlZrhKeQxAR0KxiUakXRK5WtKG4opihk,26019
34
+ mdb_engine/auth/utils.py,sha256=YkexCo0xV37mpOJUI32cntRHVOUUS7r19TIMPWHcgpA,27348
35
+ mdb_engine/auth/websocket_sessions.py,sha256=7eFNagY2K3Rp1x7d_cO5JcpT-DrYkc__cmVhl6pAC2M,15081
35
36
  mdb_engine/cli/__init__.py,sha256=PANRi4THmL34d1mawlqxIrnuItXMdqoMTq5Z1zHd7rM,301
36
37
  mdb_engine/cli/main.py,sha256=Y5ELFhvsr8zxFWv4WScOGNHiLUTdSXAJeUFLpRXCelg,811
37
38
  mdb_engine/cli/utils.py,sha256=bNRGJgdzxUjXAOVe1aoxWJ5M_IqtAE-eW4pfAkwiDDM,2760
@@ -46,13 +47,13 @@ mdb_engine/core/app_registration.py,sha256=7szt2a7aBkpSppjmhdkkPPYMKGKo0MkLKZeEe
46
47
  mdb_engine/core/app_secrets.py,sha256=bo-syg9UUATibNyXEZs-0TTYWG-JaY-2S0yNSGA12n0,10524
47
48
  mdb_engine/core/connection.py,sha256=XnwuPG34pJ7kJGJ84T0mhj1UZ6_CLz_9qZf6NRYGIS8,8346
48
49
  mdb_engine/core/encryption.py,sha256=RZ5LPF5g28E3ZBn6v1IMw_oas7u9YGFtBcEj8lTi9LM,7515
49
- mdb_engine/core/engine.py,sha256=ydYt2z53scf4GzufD8rY5X6gSJEF80VSDm1QDR2pwWQ,151507
50
+ mdb_engine/core/engine.py,sha256=s-lzI3UdmXjAsiemYUAlpVElSlHhhjPeekoQHaxbb2o,153755
50
51
  mdb_engine/core/index_management.py,sha256=9-r7MIy3JnjQ35sGqsbj8K_I07vAUWtAVgSWC99lJcE,5555
51
- mdb_engine/core/manifest.py,sha256=jguhjVPAHMZGxOJcdSGouv9_XiKmxUjDmyjn2yXHCj4,139205
52
+ mdb_engine/core/manifest.py,sha256=9g-r8HBr308rDxtpzPvZgP9Bc1VeN9pW7D073KXas-U,139989
52
53
  mdb_engine/core/ray_integration.py,sha256=vexYOzztscvRYje1xTNmXJbi99oJxCaVJAwKfTNTF_E,13610
53
54
  mdb_engine/core/seeding.py,sha256=c5IhdwlqUf_4Q5FFTAhPLaHPaUr_Txo3z_DUwZmWsFs,6421
54
55
  mdb_engine/core/service_initialization.py,sha256=rtb6BaPvFqomwT_s7bdbbvqi5m74llT0LkJFEhVG9Gg,12996
55
- mdb_engine/core/types.py,sha256=x7ahgb61vXVikn7nKHWGxQRW3StMG9lTHwr-Uvpnt_g,11150
56
+ mdb_engine/core/types.py,sha256=RGQeO8ctTBytZmiezmtffsT5kMhHKm1Dv8sz4QnjEgc,11226
56
57
  mdb_engine/database/README.md,sha256=-31mVxBeVQaYsF3AD1-gQbD2NCYVcPjdFoA6sZ6b02Y,19354
57
58
  mdb_engine/database/__init__.py,sha256=rrc3eZFli3K2zrvVdDbMBi8YkmoHYzP6JNT0AUBE5VU,981
58
59
  mdb_engine/database/abstraction.py,sha256=H6f2WYY80r3onqN6s139uDSyG9W_QpadaoQ84hJuG1E,23438
@@ -86,12 +87,12 @@ mdb_engine/repositories/mongo.py,sha256=Wg32_6v0KHAHumhz5z8QkoqJRWAMJFA7Y2lYIJ7L
86
87
  mdb_engine/repositories/unit_of_work.py,sha256=XvmwGOspEDj4hsfOULPsQKjB1QZqh83TJo6vGV4tiqU,5118
87
88
  mdb_engine/routing/README.md,sha256=WVvTQXDq0amryrjkCu0wP_piOEwFjLukjmPz2mroWHY,13658
88
89
  mdb_engine/routing/__init__.py,sha256=reupjHi_RTc2ZBA4AH5XzobAmqy4EQIsfSUcTkFknUM,2438
89
- mdb_engine/routing/websockets.py,sha256=ox9mKDVhQmKdAtdomQ99UXxp9ZrBn-OkMhR6rtZ_HiA,31887
90
+ mdb_engine/routing/websockets.py,sha256=gOYZPDCIicqRkWIVrNWTmY2ALUnZs1s0nTPmckgxgHQ,33922
90
91
  mdb_engine/utils/__init__.py,sha256=lDxQSGqkV4fVw5TWIk6FA6_eey_ZnEtMY0fir3cpAe8,236
91
92
  mdb_engine/utils/mongo.py,sha256=Oqtv4tQdpiiZzrilGLEYQPo8Vmh8WsTQypxQs8Of53s,3369
92
- mdb_engine-0.5.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
93
- mdb_engine-0.5.1.dist-info/METADATA,sha256=5_gq3GBHSvvct52eMIi7zXDZ_KOK_A3rzDSc7RXmhRU,15810
94
- mdb_engine-0.5.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
95
- mdb_engine-0.5.1.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
96
- mdb_engine-0.5.1.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
97
- mdb_engine-0.5.1.dist-info/RECORD,,
93
+ mdb_engine-0.6.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
94
+ mdb_engine-0.6.0.dist-info/METADATA,sha256=d7bW5MiTEH918H4IWkoZuDVAlZiQ6iwastveacmAN70,15810
95
+ mdb_engine-0.6.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ mdb_engine-0.6.0.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
97
+ mdb_engine-0.6.0.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
98
+ mdb_engine-0.6.0.dist-info/RECORD,,