mdb-engine 0.4.11__tar.gz → 0.4.14__tar.gz

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.
Files changed (104) hide show
  1. {mdb_engine-0.4.11/mdb_engine.egg-info → mdb_engine-0.4.14}/PKG-INFO +1 -1
  2. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/__init__.py +6 -7
  3. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/engine.py +28 -5
  4. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/routing/websockets.py +95 -55
  5. {mdb_engine-0.4.11 → mdb_engine-0.4.14/mdb_engine.egg-info}/PKG-INFO +1 -1
  6. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/pyproject.toml +1 -1
  7. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/setup.py +1 -1
  8. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/LICENSE +0 -0
  9. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/MANIFEST.in +0 -0
  10. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/README.md +0 -0
  11. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/README.md +0 -0
  12. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/ARCHITECTURE.md +0 -0
  13. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/README.md +0 -0
  14. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/__init__.py +0 -0
  15. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/audit.py +0 -0
  16. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/base.py +0 -0
  17. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/casbin_factory.py +0 -0
  18. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/casbin_models.py +0 -0
  19. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/config_defaults.py +0 -0
  20. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/config_helpers.py +0 -0
  21. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/cookie_utils.py +0 -0
  22. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/csrf.py +0 -0
  23. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/decorators.py +0 -0
  24. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/dependencies.py +0 -0
  25. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/helpers.py +0 -0
  26. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/integration.py +0 -0
  27. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/jwt.py +0 -0
  28. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/middleware.py +0 -0
  29. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/oso_factory.py +0 -0
  30. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/provider.py +0 -0
  31. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/rate_limiter.py +0 -0
  32. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/restrictions.py +0 -0
  33. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/session_manager.py +0 -0
  34. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/shared_middleware.py +0 -0
  35. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/shared_users.py +0 -0
  36. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/token_lifecycle.py +0 -0
  37. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/token_store.py +0 -0
  38. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/users.py +0 -0
  39. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/auth/utils.py +0 -0
  40. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/__init__.py +0 -0
  41. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/commands/__init__.py +0 -0
  42. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/commands/generate.py +0 -0
  43. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/commands/migrate.py +0 -0
  44. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/commands/show.py +0 -0
  45. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/commands/validate.py +0 -0
  46. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/main.py +0 -0
  47. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/cli/utils.py +0 -0
  48. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/config.py +0 -0
  49. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/constants.py +0 -0
  50. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/README.md +0 -0
  51. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/__init__.py +0 -0
  52. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/app_registration.py +0 -0
  53. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/app_secrets.py +0 -0
  54. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/connection.py +0 -0
  55. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/encryption.py +0 -0
  56. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/index_management.py +0 -0
  57. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/manifest.py +0 -0
  58. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/ray_integration.py +0 -0
  59. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/seeding.py +0 -0
  60. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/service_initialization.py +0 -0
  61. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/core/types.py +0 -0
  62. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/README.md +0 -0
  63. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/__init__.py +0 -0
  64. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/abstraction.py +0 -0
  65. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/connection.py +0 -0
  66. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/query_validator.py +0 -0
  67. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/resource_limiter.py +0 -0
  68. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/database/scoped_wrapper.py +0 -0
  69. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/dependencies.py +0 -0
  70. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/di/__init__.py +0 -0
  71. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/di/container.py +0 -0
  72. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/di/providers.py +0 -0
  73. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/di/scopes.py +0 -0
  74. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/embeddings/README.md +0 -0
  75. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/embeddings/__init__.py +0 -0
  76. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/embeddings/dependencies.py +0 -0
  77. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/embeddings/service.py +0 -0
  78. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/exceptions.py +0 -0
  79. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/indexes/README.md +0 -0
  80. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/indexes/__init__.py +0 -0
  81. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/indexes/helpers.py +0 -0
  82. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/indexes/manager.py +0 -0
  83. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/memory/README.md +0 -0
  84. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/memory/__init__.py +0 -0
  85. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/memory/service.py +0 -0
  86. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/observability/README.md +0 -0
  87. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/observability/__init__.py +0 -0
  88. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/observability/health.py +0 -0
  89. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/observability/logging.py +0 -0
  90. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/observability/metrics.py +0 -0
  91. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/repositories/__init__.py +0 -0
  92. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/repositories/base.py +0 -0
  93. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/repositories/mongo.py +0 -0
  94. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/repositories/unit_of_work.py +0 -0
  95. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/routing/README.md +0 -0
  96. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/routing/__init__.py +0 -0
  97. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/utils/__init__.py +0 -0
  98. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine/utils/mongo.py +0 -0
  99. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine.egg-info/SOURCES.txt +0 -0
  100. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine.egg-info/dependency_links.txt +0 -0
  101. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine.egg-info/entry_points.txt +0 -0
  102. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine.egg-info/requires.txt +0 -0
  103. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/mdb_engine.egg-info/top_level.txt +0 -0
  104. {mdb_engine-0.4.11 → mdb_engine-0.4.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.11
3
+ Version: 0.4.14
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -82,13 +82,12 @@ 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.4.11" # Automatic WebSocket support improvements
86
- # - Improved CORS config merging with proper wildcard handling
87
- # - WebSocket route verification after registration
88
- # - Startup verification logging for CORS config and WebSocket routes
89
- # - Enhanced CSRF middleware error messages with path and CORS status
90
- # - Better logging throughout WebSocket registration process
91
- # - All WebSocket multi-app SSO features now work automatically
85
+ "0.4.14" # WebSocket security documentation and test updates
86
+ # - Comprehensive security guide (WEBSOCKET_SECURITY_MULTI_APP_SSO.md)
87
+ # - Updated all documentation to reflect subprotocol-only authentication
88
+ # - Added integration tests for subprotocol authentication in multi-app SSO
89
+ # - Enhanced unit test documentation with security-focused explanations
90
+ # - Complete test coverage for WebSocket security scenarios
92
91
  )
93
92
 
94
93
  __all__ = [
@@ -1499,8 +1499,9 @@ class MongoDBEngine:
1499
1499
 
1500
1500
  # Add CSRF middleware (after auth - auto-enabled for shared mode)
1501
1501
  # CSRF protection is enabled by default for shared auth mode
1502
+ # SKIP for sub-apps in multi-app setups - parent app handles CSRF
1502
1503
  csrf_config = auth_config.get("csrf_protection", True if auth_mode == "shared" else False)
1503
- if csrf_config:
1504
+ if csrf_config and not is_sub_app: # Don't add CSRF to child apps
1504
1505
  from ..auth.csrf import create_csrf_middleware
1505
1506
 
1506
1507
  csrf_middleware = create_csrf_middleware(
@@ -1508,6 +1509,11 @@ class MongoDBEngine:
1508
1509
  )
1509
1510
  app.add_middleware(csrf_middleware)
1510
1511
  logger.info(f"CSRFMiddleware added for '{slug}'")
1512
+ elif csrf_config and is_sub_app:
1513
+ logger.debug(
1514
+ f"CSRFMiddleware skipped for child app '{slug}' - "
1515
+ f"parent app handles CSRF protection for WebSocket routes"
1516
+ )
1511
1517
 
1512
1518
  # Add security middleware (HSTS, headers)
1513
1519
  security_config = auth_config.get("security", {})
@@ -2127,17 +2133,33 @@ class MongoDBEngine:
2127
2133
  "Path prefix validation failed:\n" + "\n".join(f" - {e}" for e in errors)
2128
2134
  )
2129
2135
 
2130
- # Check if any app uses shared auth
2136
+ # Check if any app uses shared auth and collect public routes for CSRF exemption
2131
2137
  has_shared_auth = False
2138
+ all_public_routes = [
2139
+ "/health",
2140
+ "/docs",
2141
+ "/openapi.json",
2142
+ "/_mdb/routes",
2143
+ ] # Base exempt routes
2132
2144
  for app_config in apps:
2133
2145
  try:
2134
2146
  manifest_path = app_config["manifest"]
2147
+ path_prefix = app_config.get("path_prefix", f"/{app_config.get('slug')}")
2135
2148
  with open(manifest_path) as f:
2136
2149
  app_manifest_pre = json.load(f)
2137
2150
  auth_config = app_manifest_pre.get("auth", {})
2138
2151
  if auth_config.get("mode") == "shared":
2139
2152
  has_shared_auth = True
2140
- break
2153
+ # Collect public routes with path prefix for CSRF exemption
2154
+ child_public_routes = auth_config.get("public_routes", [])
2155
+ for route in child_public_routes:
2156
+ # Add path prefix to make route absolute on parent app
2157
+ if route.startswith("/"):
2158
+ prefixed_route = f"{path_prefix.rstrip('/')}{route}"
2159
+ else:
2160
+ prefixed_route = f"{path_prefix.rstrip('/')}/{route}"
2161
+ if prefixed_route not in all_public_routes:
2162
+ all_public_routes.append(prefixed_route)
2141
2163
  except (FileNotFoundError, json.JSONDecodeError, KeyError) as e:
2142
2164
  logger.warning(f"Could not check auth mode for app '{app_config.get('slug')}': {e}")
2143
2165
 
@@ -2812,10 +2834,11 @@ class MongoDBEngine:
2812
2834
  from ..auth.csrf import create_csrf_middleware
2813
2835
 
2814
2836
  # Create CSRF middleware with default config (will use parent app's CORS config)
2815
- # Exempt routes that don't need CSRF (health checks, etc.)
2837
+ # Exempt routes that don't need CSRF (health checks, public routes from child apps)
2838
+ # all_public_routes includes base routes + child app public routes with path prefixes
2816
2839
  parent_csrf_config = {
2817
2840
  "csrf_protection": True,
2818
- "public_routes": ["/health", "/docs", "/openapi.json", "/_mdb/routes"],
2841
+ "public_routes": all_public_routes,
2819
2842
  }
2820
2843
  csrf_middleware = create_csrf_middleware(parent_csrf_config)
2821
2844
  parent_app.add_middleware(csrf_middleware)
@@ -311,10 +311,15 @@ async def authenticate_websocket(
311
311
  websocket: Any, app_slug: str, require_auth: bool = True
312
312
  ) -> tuple[str | None, str | None]:
313
313
  """
314
- Authenticate a WebSocket connection.
314
+ Authenticate a WebSocket connection via Sec-WebSocket-Protocol header.
315
+
316
+ Uses subprotocol tunneling to pass JWT tokens securely:
317
+ - Client: new WebSocket(url, [token])
318
+ - Server: Extracts token from sec-websocket-protocol header
319
+ - Bypasses CSRF issues and avoids URL logging risks
315
320
 
316
321
  Args:
317
- websocket: FastAPI WebSocket instance
322
+ websocket: FastAPI WebSocket instance (can access headers before accept)
318
323
  app_slug: App slug for context
319
324
  require_auth: Whether authentication is required
320
325
 
@@ -335,59 +340,57 @@ async def authenticate_websocket(
335
340
  return None, None
336
341
 
337
342
  try:
338
- # Try to get token from query params or cookies
339
343
  token = None
340
- query_token = None
341
- cookie_token = None
342
-
343
- # Check query parameters
344
- # FastAPI WebSocket query params are accessed via websocket.query_params
345
- if hasattr(websocket, "query_params"):
346
- if websocket.query_params:
347
- query_token = websocket.query_params.get("token")
348
- logger.info(
349
- f"WebSocket query_params for app '{app_slug}': {dict(websocket.query_params)}"
350
- )
351
- if query_token:
352
- token = query_token
353
- logger.info(
354
- f"WebSocket token found in query params for app "
355
- f"'{app_slug}' (length: {len(query_token)})"
356
- )
357
- else:
358
- logger.info(f"WebSocket query_params is empty for app '{app_slug}'")
359
- else:
360
- logger.warning(f"WebSocket has no query_params attribute for app '{app_slug}'")
344
+ selected_subprotocol = None
361
345
 
362
- # If no token in query, try to get from cookies (if available)
363
- # Check both ws_token (non-httponly, for JS access) and token (httponly)
364
- if not token:
365
- if hasattr(websocket, "cookies"):
366
- cookie_token = websocket.cookies.get("ws_token") or websocket.cookies.get("token")
367
- if cookie_token:
368
- token = cookie_token
369
- logger.debug(f"WebSocket token found in cookies for app '{app_slug}'")
370
- else:
371
- logger.debug(f"WebSocket has no cookies attribute for app '{app_slug}'")
372
-
373
- logger.info(
374
- f"WebSocket auth check for app '{app_slug}': "
375
- f"query_token={bool(query_token)}, "
376
- f"cookie_token={bool(cookie_token)}, "
377
- f"final_token={bool(token)}, require_auth={require_auth}"
378
- )
346
+ # Sec-WebSocket-Protocol (Subprotocol Tunneling)
347
+ # Browsers allow: new WebSocket(url, ["token", "THE_JWT_STRING"])
348
+ # This bypasses CSRF issues and avoids URL logging risks
349
+ try:
350
+ protocol_header = None
351
+ # Try multiple ways to access headers (FastAPI WebSocket compatibility)
352
+ if hasattr(websocket, "headers") and websocket.headers is not None:
353
+ # FastAPI WebSocket.headers is case-insensitive dict-like
354
+ protocol_header = websocket.headers.get("sec-websocket-protocol")
355
+
356
+ # Fallback: access via scope (ASGI standard) if headers not available
357
+ if not protocol_header and hasattr(websocket, "scope") and "headers" in websocket.scope:
358
+ headers_dict = dict(websocket.scope["headers"])
359
+ # Headers are bytes in ASGI, need to decode
360
+ protocol_header_bytes = headers_dict.get(b"sec-websocket-protocol")
361
+ if protocol_header_bytes:
362
+ protocol_header = protocol_header_bytes.decode("utf-8")
363
+
364
+ if protocol_header:
365
+ # Header format: "protocol1, protocol2, ..." or just "token"
366
+ protocols = [p.strip() for p in protocol_header.split(",")]
367
+ logger.debug(f"WebSocket subprotocols for app '{app_slug}': {protocols}")
368
+
369
+ # Look for a JWT-like token in the protocols
370
+ # JWTs are typically long (>20 chars) and don't contain spaces
371
+ for protocol in protocols:
372
+ if len(protocol) > 20 and " " not in protocol:
373
+ token = protocol
374
+ selected_subprotocol = protocol
375
+ logger.info(
376
+ f"WebSocket token found in subprotocol for app '{app_slug}' "
377
+ f"(length: {len(protocol)})"
378
+ )
379
+ break
380
+ except (AttributeError, TypeError, KeyError, UnicodeDecodeError) as e:
381
+ logger.debug(f"Could not access WebSocket headers for subprotocol check: {e}")
379
382
 
380
383
  if not token:
381
384
  logger.warning(
382
- f"No token found for WebSocket connection to app '{app_slug}' "
383
- f"(require_auth={require_auth})"
385
+ f"No token found in subprotocol header for WebSocket connection to app "
386
+ f"'{app_slug}' (require_auth={require_auth}). "
387
+ f"Use: new WebSocket(url, [token]) to pass JWT token as subprotocol."
384
388
  )
385
389
  if require_auth:
386
- # Don't close before accepting - return error info instead
387
- # The caller will handle closing after accept
388
390
  return None, None # Signal auth failure
389
391
  return None, None
390
392
 
393
+ # Decode and validate token
391
394
  import jwt
392
395
 
393
396
  from ..auth.dependencies import SECRET_KEY
@@ -398,6 +401,13 @@ async def authenticate_websocket(
398
401
  user_id = payload.get("sub") or payload.get("user_id")
399
402
  user_email = payload.get("email")
400
403
 
404
+ # Store selected subprotocol on websocket scope for accept() to use
405
+ if selected_subprotocol:
406
+ # Store in scope so _accept_websocket_connection can access it
407
+ if hasattr(websocket, "scope"):
408
+ websocket.scope["_selected_subprotocol"] = selected_subprotocol
409
+ logger.debug(f"Stored subprotocol '{selected_subprotocol}' for WebSocket accept")
410
+
401
411
  logger.info(f"WebSocket authenticated successfully for app '{app_slug}': {user_email}")
402
412
  return user_id, user_email
403
413
  except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as decode_error:
@@ -409,7 +419,6 @@ async def authenticate_websocket(
409
419
  except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
410
420
  logger.error(f"WebSocket authentication failed for app '{app_slug}': {e}", exc_info=True)
411
421
  if require_auth:
412
- # Don't close before accepting - return error info instead
413
422
  return None, None # Signal auth failure
414
423
  return None, None
415
424
 
@@ -470,11 +479,32 @@ def get_message_handler(
470
479
 
471
480
 
472
481
  async def _accept_websocket_connection(websocket: Any, app_slug: str) -> None:
473
- """Accept WebSocket connection with error handling."""
482
+ """
483
+ Accept WebSocket connection with subprotocol support.
484
+
485
+ If authentication found a token in the Sec-WebSocket-Protocol header,
486
+ we must accept with that specific subprotocol or the browser will reject
487
+ the connection.
488
+ """
474
489
  try:
475
- await websocket.accept()
490
+ # Check if auth stored a selected subprotocol
491
+ selected_subprotocol = None
492
+ if hasattr(websocket, "scope"):
493
+ selected_subprotocol = websocket.scope.get("_selected_subprotocol")
494
+
495
+ if selected_subprotocol:
496
+ # Accept with the specific subprotocol the client requested
497
+ await websocket.accept(subprotocol=selected_subprotocol)
498
+ logger.info(
499
+ f"✅ WebSocket accepted for app '{app_slug}' "
500
+ f"with subprotocol '{selected_subprotocol}'"
501
+ )
502
+ else:
503
+ # Standard accept without subprotocol
504
+ await websocket.accept()
505
+ logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
506
+
476
507
  print(f"✅ [WEBSOCKET ACCEPTED] App: '{app_slug}'")
477
- logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
478
508
  except (RuntimeError, ConnectionError, OSError) as accept_error:
479
509
  print(f"❌ [WEBSOCKET ACCEPT FAILED] App: '{app_slug}', Error: {accept_error}")
480
510
  logger.error(
@@ -654,13 +684,23 @@ def create_websocket_endpoint(
654
684
  f"(require_auth={require_auth}, query_params={query_str})"
655
685
  )
656
686
 
657
- # Accept connection FIRST (required before we can do anything)
658
- await _accept_websocket_connection(websocket, app_slug)
687
+ # CRITICAL: Authenticate BEFORE accepting connection
688
+ # This prevents CSRF middleware from rejecting established connections
689
+ # We can access headers/query_params before accept() is called
690
+ user_id, user_email = await authenticate_websocket(websocket, app_slug, require_auth)
659
691
 
660
- # Authenticate connection (after accept, so we can close properly if needed)
661
- user_id, user_email = await _authenticate_websocket_connection(
662
- websocket, app_slug, require_auth
663
- )
692
+ # Handle authentication failure
693
+ if require_auth and not user_id:
694
+ logger.warning(
695
+ f"WebSocket authentication failed for app '{app_slug}' - rejecting connection"
696
+ )
697
+ # Reject without accepting - FastAPI will send 403 if accept() not called
698
+ # We can't call websocket.close() before accept(), so we just return
699
+ # The connection will be rejected by the server
700
+ return
701
+
702
+ # Accept connection (with subprotocol if token was in protocol header)
703
+ await _accept_websocket_connection(websocket, app_slug)
664
704
 
665
705
  # Connect with metadata (websocket already accepted)
666
706
  connection = await manager.connect(websocket, user_id=user_id, user_email=user_email)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.11
3
+ Version: 0.4.14
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mdb-engine"
7
- version = "0.4.11"
7
+ version = "0.4.14"
8
8
  description = "MongoDB Engine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -14,7 +14,7 @@ if readme_file.exists():
14
14
 
15
15
  setup(
16
16
  name="mdb-engine",
17
- version="0.4.11",
17
+ version="0.4.14",
18
18
  description="MongoDB Engine",
19
19
  long_description=long_description,
20
20
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes
File without changes