11"""Kinde SDK instrumentation for REPLAY mode.
22
3- This instrumentation patches StorageManager.get() to handle device ID mismatch
4- during replay. This enables successful authentication for Flask or FastAPI.
3+ This instrumentation patches Kinde SDK for replay compatibility using a two-tier approach:
54
6- Problem: During replay, Kinde's StorageManager generates a new UUID on server
7- startup, but the replayed session contains data keyed with the old device ID
8- (e.g., `device:OLD-UUID:user_id`). The lookup fails because StorageManager
9- looks for `device:NEW-UUID:user_id`.
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
109
11- Solution: Patch StorageManager.get() to:
12- 1. Try normal lookup with current device_id
13- 2. If that fails, scan session keys for pattern `device:*:{key}`
14- 3. Extract device ID from found key and cache it for future lookups
15- 4. Return the found value
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
1617
1718This approach is framework-agnostic - it works with Flask or FastAPI. Kinde does not support Django.
1819
@@ -101,11 +102,77 @@ def _scan_session_for_key(session: Any, target_key: str) -> tuple[str | None, An
101102 return None , None
102103
103104
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+
104163class KindeInstrumentation (InstrumentationBase ):
105164 """Instrumentation to patch Kinde SDK for REPLAY mode compatibility.
106165
107- Patches StorageManager.get() to handle device ID mismatch by scanning
108- session keys and extracting the correct device ID from recorded data.
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+
109176 Works with Flask, FastAPI, and other frameworks using FrameworkAwareStorage.
110177 """
111178
@@ -124,7 +191,7 @@ def __init__(self, mode: TuskDriftMode = TuskDriftMode.DISABLED, enabled: bool =
124191 super ().__init__ (
125192 name = "KindeInstrumentation" ,
126193 module_name = "kinde_sdk" ,
127- supported_versions = "* " ,
194+ supported_versions = ">=2.0.1 " ,
128195 enabled = should_enable ,
129196 )
130197
@@ -143,8 +210,12 @@ def patch(self, module: ModuleType) -> None:
143210 logger .debug ("[KindeInstrumentation] Not in REPLAY mode, skipping patch" )
144211 return
145212
213+ # Primary patch: handle device ID mismatch in StorageManager
146214 self ._patch_storage_manager_get ()
147215
216+ # Fallback patches: if StorageManager patch doesn't help, force is_authenticated to return True
217+ self ._patch_all_is_authenticated_methods ()
218+
148219 def _patch_storage_manager_get (self ) -> None :
149220 """Patch StorageManager.get() to handle device ID mismatch during replay."""
150221 try :
@@ -208,3 +279,47 @@ def patched_get(self: StorageManager, key: str) -> dict | None:
208279
209280 StorageManager .get = patched_get
210281 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" )
0 commit comments