Skip to content

Commit 444b802

Browse files
add instrumentation for kinde auth
1 parent b4b4565 commit 444b802

5 files changed

Lines changed: 230 additions & 2 deletions

File tree

drift/core/drift_sdk.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,8 +461,18 @@ def _init_auto_instrumentations(self) -> None:
461461
except ImportError:
462462
pass
463463

464-
# Socket instrumentation for detecting unpatched dependencies (REPLAY mode only)
464+
# REPLAY mode only instrumentations
465465
if self.mode == TuskDriftMode.REPLAY:
466+
# Kinde instrumentation for auth replay - registers hook for when kinde_sdk is imported
467+
try:
468+
from ..instrumentation.kinde import KindeInstrumentation
469+
470+
_ = KindeInstrumentation(mode=self.mode)
471+
logger.debug("Kinde instrumentation registered (REPLAY mode)")
472+
except Exception as e:
473+
logger.debug(f"Kinde instrumentation registration failed: {e}")
474+
475+
# Socket instrumentation for detecting unpatched dependencies
466476
try:
467477
from ..instrumentation.socket import SocketInstrumentation
468478

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Kinde SDK instrumentation for REPLAY mode."""
2+
3+
from .instrumentation import KindeInstrumentation
4+
5+
__all__ = ["KindeInstrumentation"]
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""Kinde SDK instrumentation for REPLAY mode.
2+
3+
This instrumentation patches StorageManager.get() to handle device ID mismatch
4+
during replay. This enables successful authentication across Flask, FastAPI,
5+
and other frameworks using Kinde's FrameworkAwareStorage.
6+
7+
Problem: During replay, Kinde's StorageManager generates a new UUID on server
8+
startup, but the replayed session contains data keyed with the old device ID
9+
(e.g., `device:OLD-UUID:user_id`). The lookup fails because StorageManager
10+
looks for `device:NEW-UUID:user_id`.
11+
12+
Solution: Patch StorageManager.get() to:
13+
1. Try normal lookup with current device_id
14+
2. If that fails, scan session keys for pattern `device:*:{key}`
15+
3. Extract device ID from found key and cache it for future lookups
16+
4. Return the found value
17+
18+
This approach is framework-agnostic - it works with Flask, FastAPI, or any
19+
framework using Kinde's FrameworkAwareStorage.
20+
21+
Only active in REPLAY mode.
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import logging
27+
import re
28+
from types import ModuleType
29+
from typing import TYPE_CHECKING, Any
30+
31+
from ...core.types import TuskDriftMode
32+
from ..base import InstrumentationBase
33+
34+
if TYPE_CHECKING:
35+
pass
36+
37+
logger = logging.getLogger(__name__)
38+
39+
# Pattern to extract device ID from session keys: device:{uuid}:{key}
40+
DEVICE_KEY_PATTERN = re.compile(r"^device:([^:]+):(.+)$")
41+
42+
43+
def _get_session_from_storage(storage: Any) -> Any | None:
44+
"""Get the underlying session from a storage adapter.
45+
46+
Works with FrameworkAwareStorage which supports both Flask and FastAPI.
47+
48+
FrameworkAwareStorage is a Kinde concept: kinde-python-sdk/kinde_sdk/core/storage/framework_aware_storage.py
49+
50+
Args:
51+
storage: The storage adapter instance
52+
53+
Returns:
54+
The session object if available, None otherwise.
55+
"""
56+
if storage is None:
57+
logger.debug("[KindeInstrumentation] Storage is None")
58+
return None
59+
60+
# FrameworkAwareStorage has _get_session() method
61+
if hasattr(storage, "_get_session"):
62+
try:
63+
session = storage._get_session()
64+
logger.debug(f"[KindeInstrumentation] Got session from storage._get_session(): {session is not None}")
65+
return session
66+
except Exception as e:
67+
logger.debug(f"[KindeInstrumentation] Error calling _get_session(): {e}")
68+
69+
return None
70+
71+
72+
def _scan_session_for_key(session: Any, target_key: str) -> tuple[str | None, Any | None]:
73+
"""Scan session keys to find a device-prefixed key matching the target.
74+
75+
Args:
76+
session: The session object (Flask session, FastAPI session, etc.)
77+
target_key: The key we're looking for (without device prefix)
78+
79+
Returns:
80+
Tuple of (device_id, value) if found, (None, None) otherwise.
81+
"""
82+
if session is None:
83+
return None, None
84+
85+
try:
86+
# Handle both dict-like sessions and sessions with keys() method
87+
keys = list(session.keys()) if hasattr(session, "keys") else []
88+
logger.debug(f"[KindeInstrumentation] Scanning {len(keys)} session keys for '{target_key}'")
89+
90+
for session_key in keys:
91+
match = DEVICE_KEY_PATTERN.match(session_key)
92+
if match:
93+
device_id = match.group(1)
94+
key_suffix = match.group(2)
95+
if key_suffix == target_key:
96+
value = session.get(session_key)
97+
logger.debug(f"[KindeInstrumentation] Found key '{target_key}' with device ID: {device_id}")
98+
return device_id, value
99+
except Exception as e:
100+
logger.debug(f"[KindeInstrumentation] Error scanning session: {e}")
101+
102+
logger.debug(f"[KindeInstrumentation] Key '{target_key}' not found in session")
103+
return None, None
104+
105+
106+
class KindeInstrumentation(InstrumentationBase):
107+
"""Instrumentation to patch Kinde SDK for REPLAY mode compatibility.
108+
109+
Patches StorageManager.get() to handle device ID mismatch by scanning
110+
session keys and extracting the correct device ID from recorded data.
111+
Works with Flask, FastAPI, and other frameworks using FrameworkAwareStorage.
112+
"""
113+
114+
def __init__(self, mode: TuskDriftMode = TuskDriftMode.DISABLED, enabled: bool = True) -> None:
115+
"""Initialize Kinde instrumentation.
116+
117+
Args:
118+
mode: The SDK mode (RECORD, REPLAY, DISABLED)
119+
enabled: Whether instrumentation is enabled
120+
"""
121+
self.mode = mode
122+
123+
# Only enable in REPLAY mode
124+
should_enable = enabled and mode == TuskDriftMode.REPLAY
125+
126+
super().__init__(
127+
name="KindeInstrumentation",
128+
module_name="kinde_sdk",
129+
supported_versions="*",
130+
enabled=should_enable,
131+
)
132+
133+
if should_enable:
134+
logger.debug("[KindeInstrumentation] Initialized in REPLAY mode")
135+
136+
def patch(self, module: ModuleType) -> None:
137+
"""Patch the Kinde SDK module.
138+
139+
Args:
140+
module: The kinde_sdk module to patch
141+
"""
142+
logger.debug(f"[KindeInstrumentation] patch() called with module: {module.__name__}")
143+
144+
if self.mode != TuskDriftMode.REPLAY:
145+
logger.debug("[KindeInstrumentation] Not in REPLAY mode, skipping patch")
146+
return
147+
148+
self._patch_storage_manager_get()
149+
150+
def _patch_storage_manager_get(self) -> None:
151+
"""Patch StorageManager.get() to handle device ID mismatch during replay."""
152+
try:
153+
from kinde_sdk.core.storage.storage_manager import StorageManager
154+
155+
logger.debug("[KindeInstrumentation] Successfully imported StorageManager")
156+
except ImportError as e:
157+
logger.warning(f"[KindeInstrumentation] Could not import StorageManager from kinde_sdk: {e}")
158+
return
159+
160+
original_get = StorageManager.get
161+
162+
def patched_get(self: StorageManager, key: str) -> dict | None:
163+
"""Patched get() that handles device ID mismatch.
164+
165+
First tries normal lookup. If that fails for a device-specific key,
166+
scans session for keys with different device IDs and extracts the
167+
correct device ID for future lookups.
168+
169+
Args:
170+
self: The StorageManager instance
171+
key: The key to retrieve
172+
173+
Returns:
174+
The stored data or None if not found.
175+
"""
176+
logger.debug(f"[KindeInstrumentation] patched_get() called for key: {key}")
177+
178+
# Try normal lookup first
179+
result = original_get(self, key)
180+
if result is not None:
181+
logger.debug(f"[KindeInstrumentation] Normal lookup succeeded for key: {key}")
182+
return result
183+
184+
logger.debug(f"[KindeInstrumentation] Normal lookup failed for key: {key}")
185+
186+
# Skip special keys that don't use device namespacing
187+
if key == "_device_id" or key.startswith("global:") or key.startswith("user:"):
188+
return None
189+
190+
# Normal lookup failed - try to find key with different device ID
191+
session = _get_session_from_storage(self._storage)
192+
if session is None:
193+
logger.debug("[KindeInstrumentation] Could not get session from storage")
194+
return None
195+
196+
# Scan session for this key with any device ID
197+
found_device_id, found_value = _scan_session_for_key(session, key)
198+
199+
if found_device_id and found_value is not None:
200+
# Cache the device ID for future lookups
201+
with self._lock:
202+
if self._device_id != found_device_id:
203+
logger.debug(
204+
f"[KindeInstrumentation] Updating device ID: {self._device_id} -> {found_device_id}"
205+
)
206+
self._device_id = found_device_id
207+
return found_value
208+
209+
return None
210+
211+
StorageManager.get = patched_get
212+
logger.debug("[KindeInstrumentation] Patched StorageManager.get()")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ include = [
123123
"drift/instrumentation/psycopg/**",
124124
"drift/instrumentation/psycopg2/**",
125125
"drift/instrumentation/redis/**",
126+
"drift/instrumentation/kinde/**",
126127
"drift/instrumentation/http/transform_engine.py",
127128
]
128129

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)