mdb-engine 0.4.14__tar.gz → 0.5.1__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.14/mdb_engine.egg-info → mdb_engine-0.5.1}/PKG-INFO +1 -1
  2. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/__init__.py +7 -4
  3. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/cookie_utils.py +13 -3
  4. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/csrf.py +57 -4
  5. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/routing/websockets.py +104 -81
  6. {mdb_engine-0.4.14 → mdb_engine-0.5.1/mdb_engine.egg-info}/PKG-INFO +1 -1
  7. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/pyproject.toml +1 -1
  8. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/setup.py +1 -1
  9. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/LICENSE +0 -0
  10. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/MANIFEST.in +0 -0
  11. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/README.md +0 -0
  12. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/README.md +0 -0
  13. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/ARCHITECTURE.md +0 -0
  14. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/README.md +0 -0
  15. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/__init__.py +0 -0
  16. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/audit.py +0 -0
  17. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/base.py +0 -0
  18. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/casbin_factory.py +0 -0
  19. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/casbin_models.py +0 -0
  20. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/config_defaults.py +0 -0
  21. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/config_helpers.py +0 -0
  22. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/decorators.py +0 -0
  23. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/dependencies.py +0 -0
  24. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/helpers.py +0 -0
  25. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/integration.py +0 -0
  26. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/jwt.py +0 -0
  27. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/middleware.py +0 -0
  28. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/oso_factory.py +0 -0
  29. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/provider.py +0 -0
  30. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/rate_limiter.py +0 -0
  31. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/restrictions.py +0 -0
  32. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/session_manager.py +0 -0
  33. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/shared_middleware.py +0 -0
  34. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/shared_users.py +0 -0
  35. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/token_lifecycle.py +0 -0
  36. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/token_store.py +0 -0
  37. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/users.py +0 -0
  38. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/auth/utils.py +0 -0
  39. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/__init__.py +0 -0
  40. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/commands/__init__.py +0 -0
  41. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/commands/generate.py +0 -0
  42. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/commands/migrate.py +0 -0
  43. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/commands/show.py +0 -0
  44. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/commands/validate.py +0 -0
  45. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/main.py +0 -0
  46. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/cli/utils.py +0 -0
  47. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/config.py +0 -0
  48. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/constants.py +0 -0
  49. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/README.md +0 -0
  50. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/__init__.py +0 -0
  51. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/app_registration.py +0 -0
  52. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/app_secrets.py +0 -0
  53. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/connection.py +0 -0
  54. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/encryption.py +0 -0
  55. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/engine.py +0 -0
  56. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/index_management.py +0 -0
  57. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/manifest.py +0 -0
  58. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/ray_integration.py +0 -0
  59. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/seeding.py +0 -0
  60. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/service_initialization.py +0 -0
  61. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/core/types.py +0 -0
  62. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/README.md +0 -0
  63. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/__init__.py +0 -0
  64. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/abstraction.py +0 -0
  65. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/connection.py +0 -0
  66. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/query_validator.py +0 -0
  67. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/resource_limiter.py +0 -0
  68. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/database/scoped_wrapper.py +0 -0
  69. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/dependencies.py +0 -0
  70. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/di/__init__.py +0 -0
  71. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/di/container.py +0 -0
  72. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/di/providers.py +0 -0
  73. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/di/scopes.py +0 -0
  74. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/embeddings/README.md +0 -0
  75. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/embeddings/__init__.py +0 -0
  76. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/embeddings/dependencies.py +0 -0
  77. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/embeddings/service.py +0 -0
  78. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/exceptions.py +0 -0
  79. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/indexes/README.md +0 -0
  80. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/indexes/__init__.py +0 -0
  81. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/indexes/helpers.py +0 -0
  82. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/indexes/manager.py +0 -0
  83. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/memory/README.md +0 -0
  84. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/memory/__init__.py +0 -0
  85. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/memory/service.py +0 -0
  86. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/observability/README.md +0 -0
  87. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/observability/__init__.py +0 -0
  88. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/observability/health.py +0 -0
  89. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/observability/logging.py +0 -0
  90. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/observability/metrics.py +0 -0
  91. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/repositories/__init__.py +0 -0
  92. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/repositories/base.py +0 -0
  93. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/repositories/mongo.py +0 -0
  94. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/repositories/unit_of_work.py +0 -0
  95. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/routing/README.md +0 -0
  96. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/routing/__init__.py +0 -0
  97. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/utils/__init__.py +0 -0
  98. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine/utils/mongo.py +0 -0
  99. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine.egg-info/SOURCES.txt +0 -0
  100. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine.egg-info/dependency_links.txt +0 -0
  101. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine.egg-info/entry_points.txt +0 -0
  102. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine.egg-info/requires.txt +0 -0
  103. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/mdb_engine.egg-info/top_level.txt +0 -0
  104. {mdb_engine-0.4.14 → mdb_engine-0.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.14
3
+ Version: 0.5.1
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -82,12 +82,15 @@ 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.14" # WebSocket security documentation and test updates
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
86
88
  # - 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
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
90
92
  # - Complete test coverage for WebSocket security scenarios
93
+ # - Multi-app SSO compatibility with path="/" cookies
91
94
  )
92
95
 
93
96
  __all__ = [
@@ -116,8 +116,14 @@ def set_auth_cookies(
116
116
  refresh_token_ttl = 604800 # Default 7 days
117
117
 
118
118
  # Set access token cookie
119
+ # CRITICAL: path="/" ensures cookie is available to all mounted sub-apps
120
+ # Without explicit path, cookies default to request path and won't work with app.mount()
119
121
  response.set_cookie(
120
- key="token", value=access_token, max_age=access_token_ttl, **cookie_settings
122
+ key="token",
123
+ value=access_token,
124
+ max_age=access_token_ttl,
125
+ path="/", # Required for FastAPI mounted apps
126
+ **cookie_settings,
121
127
  )
122
128
 
123
129
  # Set refresh token cookie if provided
@@ -126,6 +132,7 @@ def set_auth_cookies(
126
132
  key="refresh_token",
127
133
  value=refresh_token,
128
134
  max_age=refresh_token_ttl,
135
+ path="/", # Required for FastAPI mounted apps
129
136
  **cookie_settings,
130
137
  )
131
138
 
@@ -148,7 +155,10 @@ def clear_auth_cookies(response, request: Request | None = None):
148
155
  secure = os.getenv("G_NOME_ENV") == "production"
149
156
 
150
157
  # Delete access token cookie
151
- response.delete_cookie(key="token", httponly=True, secure=secure, samesite=samesite)
158
+ # CRITICAL: path="/" ensures cookie deletion works across all mounted sub-apps
159
+ response.delete_cookie(key="token", httponly=True, secure=secure, samesite=samesite, path="/")
152
160
 
153
161
  # Delete refresh token cookie
154
- response.delete_cookie(key="refresh_token", httponly=True, secure=secure, samesite=samesite)
162
+ response.delete_cookie(
163
+ key="refresh_token", httponly=True, secure=secure, samesite=samesite, path="/"
164
+ )
@@ -299,9 +299,9 @@ class CSRFMiddleware(BaseHTTPMiddleware):
299
299
  method = request.method
300
300
 
301
301
  # CRITICAL: Handle WebSocket upgrade requests BEFORE other CSRF checks
302
- # WebSocket upgrades don't use CSRF tokens, but need origin validation
302
+ # WebSocket upgrades use cookie-based authentication and require CSRF validation
303
303
  if self._is_websocket_upgrade(request):
304
- # Validate origin for WebSocket connections (CSWSH protection)
304
+ # Always validate origin for WebSocket connections (CSWSH protection)
305
305
  if not self._validate_websocket_origin(request):
306
306
  logger.warning(
307
307
  f"WebSocket origin validation failed for {path}: "
@@ -312,8 +312,61 @@ class CSRFMiddleware(BaseHTTPMiddleware):
312
312
  status_code=status.HTTP_403_FORBIDDEN,
313
313
  content={"detail": "Invalid origin for WebSocket connection"},
314
314
  )
315
- # Origin validated - allow WebSocket upgrade to proceed
316
- # No CSRF token check needed for WebSocket upgrades
315
+
316
+ # Cookie-based authentication requires CSRF protection
317
+ # Check if authentication token cookie is present
318
+ # Use same cookie name as SharedAuthMiddleware for consistency
319
+ from .shared_middleware import AUTH_COOKIE_NAME
320
+
321
+ auth_token_cookie = request.cookies.get(AUTH_COOKIE_NAME)
322
+ if auth_token_cookie:
323
+ # For WebSocket upgrades, CSRF protection relies on:
324
+ # 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
327
+ #
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"},
337
+ )
338
+
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
+ },
349
+ )
350
+
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
+ logger.debug(
364
+ f"WebSocket upgrade CSRF validation passed for {path} "
365
+ f"(Origin validated, CSRF cookie present)"
366
+ )
367
+
368
+ # Origin validated (and CSRF validated if authenticated)
369
+ # Allow WebSocket upgrade to proceed
317
370
  return await call_next(request)
318
371
 
319
372
  if self._is_exempt(path):
@@ -307,16 +307,70 @@ def get_websocket_manager_sync(app_slug: str) -> WebSocketConnectionManager:
307
307
  return _websocket_managers[app_slug]
308
308
 
309
309
 
310
+ def _get_cookies_from_websocket(websocket: Any) -> dict[str, str]:
311
+ """
312
+ Extract cookies from WebSocket request.
313
+
314
+ Supports both FastAPI WebSocket .cookies attribute and ASGI scope Cookie header.
315
+
316
+ Args:
317
+ websocket: FastAPI WebSocket instance
318
+
319
+ Returns:
320
+ Dictionary of cookie name -> cookie value
321
+ """
322
+ cookies: dict[str, str] = {}
323
+
324
+ try:
325
+ # Try FastAPI WebSocket .cookies attribute (if available)
326
+ if hasattr(websocket, "cookies") and websocket.cookies is not None:
327
+ # FastAPI WebSocket.cookies is a dict-like object
328
+ cookies = dict(websocket.cookies)
329
+ logger.debug(f"Extracted {len(cookies)} cookies from WebSocket.cookies attribute")
330
+ return cookies
331
+ except (AttributeError, TypeError) as e:
332
+ logger.debug(f"Could not access WebSocket.cookies attribute: {e}")
333
+
334
+ try:
335
+ # Fallback: Extract from Cookie header in ASGI scope
336
+ if hasattr(websocket, "scope") and "headers" in websocket.scope:
337
+ headers_dict = dict(websocket.scope["headers"])
338
+ cookie_header_bytes = headers_dict.get(b"cookie")
339
+ if not cookie_header_bytes:
340
+ # Try case-insensitive lookup
341
+ for key, value in headers_dict.items():
342
+ if isinstance(key, bytes) and key.lower() == b"cookie":
343
+ cookie_header_bytes = value
344
+ break
345
+
346
+ if cookie_header_bytes:
347
+ cookie_header = cookie_header_bytes.decode("utf-8")
348
+ # Parse cookie string: "name1=value1; name2=value2"
349
+ for cookie_pair in cookie_header.split(";"):
350
+ cookie_pair = cookie_pair.strip()
351
+ if "=" in cookie_pair:
352
+ name, value = cookie_pair.split("=", 1)
353
+ cookies[name.strip()] = value.strip()
354
+ logger.debug(f"Extracted {len(cookies)} cookies from Cookie header in ASGI scope")
355
+ return cookies
356
+ except (AttributeError, TypeError, KeyError, UnicodeDecodeError, ValueError) as e:
357
+ logger.debug(f"Could not extract cookies from ASGI scope: {e}")
358
+
359
+ return cookies
360
+
361
+
310
362
  async def authenticate_websocket(
311
- websocket: Any, app_slug: str, require_auth: bool = True
363
+ websocket: Any,
364
+ app_slug: str,
365
+ require_auth: bool = True,
312
366
  ) -> tuple[str | None, str | None]:
313
367
  """
314
- Authenticate a WebSocket connection via Sec-WebSocket-Protocol header.
368
+ Authenticate a WebSocket connection via httpOnly cookies.
315
369
 
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
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
320
374
 
321
375
  Args:
322
376
  websocket: FastAPI WebSocket instance (can access headers before accept)
@@ -340,56 +394,28 @@ async def authenticate_websocket(
340
394
  return None, None
341
395
 
342
396
  try:
343
- token = None
344
- selected_subprotocol = None
397
+ # Extract token from httpOnly cookie
398
+ # Use same cookie name as SharedAuthMiddleware for consistency
399
+ from ..auth.shared_middleware import AUTH_COOKIE_NAME
345
400
 
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}")
401
+ cookies = _get_cookies_from_websocket(websocket)
402
+ token = cookies.get(AUTH_COOKIE_NAME) # Use mdb_auth_token (same as shared middleware)
382
403
 
383
404
  if not token:
384
- logger.warning(
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."
405
+ logger.error(
406
+ f"No token cookie found for WebSocket connection to app '{app_slug}' "
407
+ f"(require_auth={require_auth}). "
408
+ f"Available cookies: {list(cookies.keys()) if cookies else 'none'}. "
409
+ f"Ensure httpOnly cookie is set during authentication."
388
410
  )
389
411
  if require_auth:
390
412
  return None, None # Signal auth failure
391
413
  return None, None
392
414
 
415
+ logger.info(
416
+ f"WebSocket token found in cookie for app '{app_slug}' " "(cookie-based authentication)"
417
+ )
418
+
393
419
  # Decode and validate token
394
420
  import jwt
395
421
 
@@ -401,17 +427,16 @@ async def authenticate_websocket(
401
427
  user_id = payload.get("sub") or payload.get("user_id")
402
428
  user_email = payload.get("email")
403
429
 
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
-
411
- logger.info(f"WebSocket authenticated successfully for app '{app_slug}': {user_email}")
430
+ logger.info(
431
+ f"WebSocket authenticated successfully for app '{app_slug}': {user_email} "
432
+ f"(method: cookie)"
433
+ )
412
434
  return user_id, user_email
413
- except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as decode_error:
414
- logger.error(f"JWT decode error for app '{app_slug}': {decode_error}", exc_info=True)
435
+ except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
436
+ logger.exception(
437
+ f"❌ JWT decode error for app '{app_slug}'. "
438
+ f"Token present: {bool(token)}, Token length: {len(token) if token else 0}"
439
+ )
415
440
  raise
416
441
 
417
442
  except WebSocketDisconnect:
@@ -480,29 +505,13 @@ def get_message_handler(
480
505
 
481
506
  async def _accept_websocket_connection(websocket: Any, app_slug: str) -> None:
482
507
  """
483
- Accept WebSocket connection with subprotocol support.
508
+ Accept WebSocket connection.
484
509
 
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.
510
+ Cookie-based authentication uses httpOnly cookies automatically sent by the browser.
488
511
  """
489
512
  try:
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}'")
513
+ await websocket.accept()
514
+ logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
506
515
 
507
516
  print(f"✅ [WEBSOCKET ACCEPTED] App: '{app_slug}'")
508
517
  except (RuntimeError, ConnectionError, OSError) as accept_error:
@@ -687,19 +696,33 @@ def create_websocket_endpoint(
687
696
  # CRITICAL: Authenticate BEFORE accepting connection
688
697
  # This prevents CSRF middleware from rejecting established connections
689
698
  # We can access headers/query_params before accept() is called
699
+
700
+ # Debug: Log cookies before authentication
701
+ try:
702
+ cookies = _get_cookies_from_websocket(websocket)
703
+ cookie_names = list(cookies.keys()) if cookies else []
704
+ logger.info(
705
+ f"🔍 WebSocket cookies for app '{app_slug}': {cookie_names} "
706
+ f"(require_auth={require_auth})"
707
+ )
708
+ except (AttributeError, TypeError, KeyError, RuntimeError) as cookie_error:
709
+ logger.warning(f"Could not extract cookies for debugging: {cookie_error}")
710
+
690
711
  user_id, user_email = await authenticate_websocket(websocket, app_slug, require_auth)
691
712
 
692
713
  # Handle authentication failure
693
714
  if require_auth and not user_id:
694
- logger.warning(
695
- f"WebSocket authentication failed for app '{app_slug}' - rejecting connection"
715
+ logger.error(
716
+ f"WebSocket authentication FAILED for app '{app_slug}' - "
717
+ f"rejecting connection. require_auth={require_auth}, "
718
+ f"user_id={user_id}, user_email={user_email}"
696
719
  )
697
720
  # Reject without accepting - FastAPI will send 403 if accept() not called
698
721
  # We can't call websocket.close() before accept(), so we just return
699
722
  # The connection will be rejected by the server
700
723
  return
701
724
 
702
- # Accept connection (with subprotocol if token was in protocol header)
725
+ # Accept connection
703
726
  await _accept_websocket_connection(websocket, app_slug)
704
727
 
705
728
  # Connect with metadata (websocket already accepted)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.14
3
+ Version: 0.5.1
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.14"
7
+ version = "0.5.1"
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.14",
17
+ version="0.5.1",
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