Skip to content

Commit 5d97f8e

Browse files
feat: instrumentation for kinde auth (#39)
1 parent 4b4b1f3 commit 5d97f8e

5 files changed

Lines changed: 343 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Tusk Drift currently supports the following packages and versions:
5555
| psycopg | `>=3.1.12` |
5656
| psycopg2 | all versions |
5757
| Redis | `>=4.0.0` |
58+
| Kinde | `>=2.0.1` |
5859

5960
If you're using packages or versions not listed above, please create an issue with the package + version you'd like an instrumentation for.
6061

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: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
"""Kinde SDK instrumentation for REPLAY mode.
2+
3+
This instrumentation patches Kinde SDK for replay compatibility using a two-tier approach:
4+
5+
1. PRIMARY: Patch StorageManager.get() to handle device ID mismatch
6+
- During replay, Kinde's StorageManager generates a new UUID on server startup
7+
- But the replayed session contains data keyed with the old device ID
8+
- This patch scans session keys for pattern `device:*:{key}` and extracts the correct device ID
9+
10+
2. FALLBACK: Patch all is_authenticated() methods/functions to return True
11+
- OAuth.is_authenticated()
12+
- UserSession.is_authenticated()
13+
- Tokens.is_authenticated()
14+
- helpers.is_authenticated()
15+
- If StorageManager patch doesn't help (e.g., app stores auth state elsewhere)
16+
- Return True anyway since we're replaying known-good authenticated requests
17+
18+
This approach is framework-agnostic - it works with Flask or FastAPI. Kinde does not support Django.
19+
20+
Only active in REPLAY mode.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import logging
26+
import re
27+
from types import ModuleType
28+
from typing import TYPE_CHECKING, Any
29+
30+
from ...core.types import TuskDriftMode
31+
from ..base import InstrumentationBase
32+
33+
if TYPE_CHECKING:
34+
pass
35+
36+
logger = logging.getLogger(__name__)
37+
38+
# Pattern to extract device ID from session keys: device:{uuid}:{key}
39+
DEVICE_KEY_PATTERN = re.compile(r"^device:([^:]+):(.+)$")
40+
41+
42+
def _get_session_from_storage(storage: Any) -> Any | None:
43+
"""Get the underlying session from a storage adapter.
44+
45+
Works with FrameworkAwareStorage which supports both Flask and FastAPI.
46+
47+
FrameworkAwareStorage is a Kinde concept: kinde-python-sdk/kinde_sdk/core/storage/framework_aware_storage.py
48+
49+
Args:
50+
storage: The storage adapter instance
51+
52+
Returns:
53+
The session object if available, None otherwise.
54+
"""
55+
if storage is None:
56+
logger.debug("[KindeInstrumentation] Storage is None")
57+
return None
58+
59+
# FrameworkAwareStorage has _get_session() method
60+
if hasattr(storage, "_get_session"):
61+
try:
62+
session = storage._get_session()
63+
logger.debug(f"[KindeInstrumentation] Got session from storage._get_session(): {session is not None}")
64+
return session
65+
except Exception as e:
66+
logger.debug(f"[KindeInstrumentation] Error calling _get_session(): {e}")
67+
68+
return None
69+
70+
71+
def _scan_session_for_key(session: Any, target_key: str) -> tuple[str | None, Any | None]:
72+
"""Scan session keys to find a device-prefixed key matching the target.
73+
74+
Args:
75+
session: The session object (Flask session, FastAPI session, etc.)
76+
target_key: The key we're looking for (without device prefix)
77+
78+
Returns:
79+
Tuple of (device_id, value) if found, (None, None) otherwise.
80+
"""
81+
if session is None:
82+
return None, None
83+
84+
try:
85+
# Handle both dict-like sessions and sessions with keys() method
86+
keys = list(session.keys()) if hasattr(session, "keys") else []
87+
logger.debug(f"[KindeInstrumentation] Scanning {len(keys)} session keys for '{target_key}'")
88+
89+
for session_key in keys:
90+
match = DEVICE_KEY_PATTERN.match(session_key)
91+
if match:
92+
device_id = match.group(1)
93+
key_suffix = match.group(2)
94+
if key_suffix == target_key:
95+
value = session.get(session_key)
96+
logger.debug(f"[KindeInstrumentation] Found key '{target_key}' with device ID: {device_id}")
97+
return device_id, value
98+
except Exception as e:
99+
logger.debug(f"[KindeInstrumentation] Error scanning session: {e}")
100+
101+
logger.debug(f"[KindeInstrumentation] Key '{target_key}' not found in session")
102+
return None, None
103+
104+
105+
def _patch_is_authenticated_method(cls: type, method_name: str, class_name: str) -> bool:
106+
"""Patch an is_authenticated method on a class to return True as fallback.
107+
108+
Args:
109+
cls: The class containing the method
110+
method_name: Name of the method to patch
111+
class_name: Display name for logging
112+
113+
Returns:
114+
True if patching succeeded, False otherwise.
115+
"""
116+
original = getattr(cls, method_name)
117+
118+
def patched(*args: Any, **kwargs: Any) -> bool:
119+
result = original(*args, **kwargs)
120+
if result:
121+
logger.debug(f"[KindeInstrumentation] {class_name}.{method_name}() returned True")
122+
return True
123+
logger.debug(
124+
f"[KindeInstrumentation] {class_name}.{method_name}() returned False, "
125+
"using REPLAY mode fallback (returning True)"
126+
)
127+
return True
128+
129+
setattr(cls, method_name, patched)
130+
logger.debug(f"[KindeInstrumentation] Patched {class_name}.{method_name}()")
131+
return True
132+
133+
134+
def _patch_is_authenticated_function(module: ModuleType, func_name: str, module_name: str) -> bool:
135+
"""Patch a standalone is_authenticated function to return True as fallback.
136+
137+
Args:
138+
module: The module containing the function
139+
func_name: Name of the function to patch
140+
module_name: Display name for logging
141+
142+
Returns:
143+
True if patching succeeded, False otherwise.
144+
"""
145+
original = getattr(module, func_name)
146+
147+
def patched(*args: Any, **kwargs: Any) -> bool:
148+
result = original(*args, **kwargs)
149+
if result:
150+
logger.debug(f"[KindeInstrumentation] {module_name}.{func_name}() returned True")
151+
return True
152+
logger.debug(
153+
f"[KindeInstrumentation] {module_name}.{func_name}() returned False, "
154+
"using REPLAY mode fallback (returning True)"
155+
)
156+
return True
157+
158+
setattr(module, func_name, patched)
159+
logger.debug(f"[KindeInstrumentation] Patched {module_name}.{func_name}()")
160+
return True
161+
162+
163+
class KindeInstrumentation(InstrumentationBase):
164+
"""Instrumentation to patch Kinde SDK for REPLAY mode compatibility.
165+
166+
Uses a two-tier approach:
167+
1. Patches StorageManager.get() to handle device ID mismatch by scanning
168+
session keys and extracting the correct device ID from recorded data.
169+
2. Patches all is_authenticated() methods/functions as a fallback to return
170+
True if the StorageManager approach doesn't work:
171+
- OAuth.is_authenticated()
172+
- UserSession.is_authenticated()
173+
- Tokens.is_authenticated()
174+
- helpers.is_authenticated()
175+
176+
Works with Flask, FastAPI, and other frameworks using FrameworkAwareStorage.
177+
"""
178+
179+
def __init__(self, mode: TuskDriftMode = TuskDriftMode.DISABLED, enabled: bool = True) -> None:
180+
"""Initialize Kinde instrumentation.
181+
182+
Args:
183+
mode: The SDK mode (RECORD, REPLAY, DISABLED)
184+
enabled: Whether instrumentation is enabled
185+
"""
186+
self.mode = mode
187+
188+
# Only enable in REPLAY mode
189+
should_enable = enabled and mode == TuskDriftMode.REPLAY
190+
191+
super().__init__(
192+
name="KindeInstrumentation",
193+
module_name="kinde_sdk",
194+
supported_versions=">=2.0.1",
195+
enabled=should_enable,
196+
)
197+
198+
if should_enable:
199+
logger.debug("[KindeInstrumentation] Initialized in REPLAY mode")
200+
201+
def patch(self, module: ModuleType) -> None:
202+
"""Patch the Kinde SDK module.
203+
204+
Args:
205+
module: The kinde_sdk module to patch
206+
"""
207+
logger.debug(f"[KindeInstrumentation] patch() called with module: {module.__name__}")
208+
209+
if self.mode != TuskDriftMode.REPLAY:
210+
logger.debug("[KindeInstrumentation] Not in REPLAY mode, skipping patch")
211+
return
212+
213+
# Primary patch: handle device ID mismatch in StorageManager
214+
self._patch_storage_manager_get()
215+
216+
# Fallback patches: if StorageManager patch doesn't help, force is_authenticated to return True
217+
self._patch_all_is_authenticated_methods()
218+
219+
def _patch_storage_manager_get(self) -> None:
220+
"""Patch StorageManager.get() to handle device ID mismatch during replay."""
221+
try:
222+
from kinde_sdk.core.storage.storage_manager import StorageManager
223+
224+
logger.debug("[KindeInstrumentation] Successfully imported StorageManager")
225+
except ImportError as e:
226+
logger.warning(f"[KindeInstrumentation] Could not import StorageManager from kinde_sdk: {e}")
227+
return
228+
229+
original_get = StorageManager.get
230+
231+
def patched_get(self: StorageManager, key: str) -> dict | None:
232+
"""Patched get() that handles device ID mismatch.
233+
234+
First tries normal lookup. If that fails for a device-specific key,
235+
scans session for keys with different device IDs and extracts the
236+
correct device ID for future lookups.
237+
238+
Args:
239+
self: The StorageManager instance
240+
key: The key to retrieve
241+
242+
Returns:
243+
The stored data or None if not found.
244+
"""
245+
logger.debug(f"[KindeInstrumentation] patched_get() called for key: {key}")
246+
247+
# Try normal lookup first
248+
result = original_get(self, key)
249+
if result is not None:
250+
logger.debug(f"[KindeInstrumentation] Normal lookup succeeded for key: {key}")
251+
return result
252+
253+
logger.debug(f"[KindeInstrumentation] Normal lookup failed for key: {key}")
254+
255+
# Skip special keys that don't use device namespacing
256+
if key == "_device_id" or key.startswith("global:") or key.startswith("user:"):
257+
return None
258+
259+
# Normal lookup failed - try to find key with different device ID
260+
session = _get_session_from_storage(self._storage)
261+
if session is None:
262+
logger.debug("[KindeInstrumentation] Could not get session from storage")
263+
return None
264+
265+
# Scan session for this key with any device ID
266+
found_device_id, found_value = _scan_session_for_key(session, key)
267+
268+
if found_device_id and found_value is not None:
269+
# Cache the device ID for future lookups
270+
with self._lock:
271+
if self._device_id != found_device_id:
272+
logger.debug(
273+
f"[KindeInstrumentation] Updating device ID: {self._device_id} -> {found_device_id}"
274+
)
275+
self._device_id = found_device_id
276+
return found_value
277+
278+
return None
279+
280+
StorageManager.get = patched_get
281+
logger.debug("[KindeInstrumentation] Patched StorageManager.get()")
282+
283+
def _patch_all_is_authenticated_methods(self) -> None:
284+
"""Patch all is_authenticated methods/functions in Kinde SDK.
285+
286+
This patches:
287+
- OAuth.is_authenticated()
288+
- UserSession.is_authenticated()
289+
- Tokens.is_authenticated()
290+
- helpers.is_authenticated()
291+
292+
Each patch wraps the original to return True as a fallback when the
293+
original returns False, since we're replaying known-good authenticated requests.
294+
"""
295+
# Patch OAuth.is_authenticated
296+
try:
297+
from kinde_sdk.auth.oauth import OAuth
298+
299+
_patch_is_authenticated_method(OAuth, "is_authenticated", "OAuth")
300+
except ImportError:
301+
logger.debug("[KindeInstrumentation] Could not import OAuth, skipping patch")
302+
303+
# Patch UserSession.is_authenticated
304+
try:
305+
from kinde_sdk.auth.user_session import UserSession
306+
307+
_patch_is_authenticated_method(UserSession, "is_authenticated", "UserSession")
308+
except ImportError:
309+
logger.debug("[KindeInstrumentation] Could not import UserSession, skipping patch")
310+
311+
# Patch Tokens.is_authenticated
312+
try:
313+
from kinde_sdk.auth.tokens import Tokens
314+
315+
_patch_is_authenticated_method(Tokens, "is_authenticated", "Tokens")
316+
except ImportError:
317+
logger.debug("[KindeInstrumentation] Could not import Tokens, skipping patch")
318+
319+
# Patch helpers.is_authenticated (standalone function)
320+
try:
321+
from kinde_sdk.core import helpers
322+
323+
_patch_is_authenticated_function(helpers, "is_authenticated", "helpers")
324+
except ImportError:
325+
logger.debug("[KindeInstrumentation] Could not import helpers, skipping patch")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ include = [
129129
"drift/instrumentation/psycopg/**",
130130
"drift/instrumentation/psycopg2/**",
131131
"drift/instrumentation/redis/**",
132+
"drift/instrumentation/kinde/**",
132133
"drift/instrumentation/http/transform_engine.py",
133134
]
134135

0 commit comments

Comments
 (0)