Skip to content

Commit b5861ae

Browse files
Syed Raza AbbasSyed Raza Abbas
authored andcommitted
added mechanism for clash safety of between sessions and added readme file
1 parent e5aa0fd commit b5861ae

File tree

3 files changed

+227
-5
lines changed

3 files changed

+227
-5
lines changed

README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,118 @@ You can also use the [Python starter kit here](https://github.com/kinde-starter-
1010

1111
For details on integrating this SDK into your project, head over to the [Kinde docs](https://kinde.com/docs/) and see the [Python SDK](https://kinde.com/docs/developer-tools/python-sdk/) doc 👍🏼.
1212

13+
## Storage Usage Examples
14+
15+
### Basic Usage
16+
```python
17+
from kinde_sdk.auth import OAuth
18+
from kinde_sdk.core.storage import StorageManager
19+
20+
# Basic initialization via OAuth
21+
# This is the recommended way to initialize the storage system
22+
# OAuth automatically initializes the StorageManager with the provided config
23+
oauth = OAuth(
24+
client_id="your_client_id",
25+
client_secret="your_client_secret",
26+
redirect_uri="your_redirect_uri"
27+
)
28+
29+
# Direct access to the storage manager
30+
# This is safe to use after OAuth initialization
31+
storage_manager = StorageManager()
32+
33+
# Store authentication data
34+
storage_manager.set("user_tokens", {
35+
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
36+
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
37+
"expires_at": 1678901234
38+
})
39+
40+
# Retrieve tokens
41+
tokens = storage_manager.get("user_tokens")
42+
if tokens:
43+
access_token = tokens.get("access_token")
44+
# Use the access token for API requests
45+
46+
# Delete tokens when logging out
47+
storage_manager.delete("user_tokens")
48+
```
49+
50+
### Using a Custom Storage Backend
51+
```python
52+
oauth = OAuth(
53+
client_id="your_client_id",
54+
storage_config={
55+
"type": "local_storage",
56+
"options": {
57+
# backend-specific options
58+
}
59+
}
60+
)
61+
```
62+
63+
### Handling Multi-Device Usage
64+
The StorageManager automatically assigns a unique device ID to each client instance, ensuring that
65+
the same user logged in on different devices won't experience session clashes. Keys are namespaced
66+
with the device ID by default.
67+
68+
```python
69+
# Get the current device ID
70+
device_id = storage_manager.get_device_id()
71+
print(f"Current device ID: {device_id}")
72+
73+
# Clear all data for the current device (useful for logout)
74+
storage_manager.clear_device_data()
75+
76+
# For data that should be shared across all devices for the same user
77+
# Use the "user:" prefix
78+
storage_manager.set("user:shared_preferences", {"theme": "dark"})
79+
80+
# For data that should be global across all users and devices
81+
# Use the "global:" prefix
82+
storage_manager.set("global:app_settings", {"version": "1.0.0"})
83+
```
84+
85+
## Best Practices for Storage Management
86+
87+
1. **Always initialize OAuth first**: The OAuth constructor initializes the StorageManager, so create your OAuth instance before accessing the storage.
88+
89+
2. **Manual initialization (if needed)**: If you need to use StorageManager before creating an OAuth instance, explicitly initialize it first:
90+
```python
91+
# Manual initialization
92+
storage_manager = StorageManager()
93+
storage_manager.initialize({"type": "memory"}) # or your preferred storage config
94+
95+
# You can also provide a specific device ID
96+
storage_manager.initialize(
97+
config={"type": "memory"},
98+
device_id="custom-device-identifier"
99+
)
100+
101+
# Now safe to use
102+
storage_manager.set("some_key", {"some": "value"})
103+
```
104+
105+
3. **Safe access pattern**: If you're unsure about initialization status, you can use this pattern:
106+
```python
107+
storage_manager = StorageManager()
108+
if not storage_manager._initialized:
109+
storage_manager.initialize()
110+
111+
# Now safe to use
112+
data = storage_manager.get("some_key")
113+
```
114+
115+
4. **Single configuration**: Configure the storage only once at application startup. Changing storage configuration mid-operation may lead to data inconsistency.
116+
117+
5. **Access from anywhere**: After initialization, you can safely access the StorageManager from any part of your application without passing it around.
118+
119+
6. **Device-specific data**: Understand that by default, data is stored with device-specific namespacing. To share data across devices, use the appropriate prefixes.
120+
121+
7. **Complete logout**: To ensure all device-specific data is cleared during logout, call `storage_manager.clear_device_data()`.
122+
123+
124+
13125
## Publishing
14126

15127
The core team handles publishing.

kinde_sdk/auth/user_session.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ def set_user_data(self, user_id: str, user_info: Dict[str, Any], token_data: Dic
5252
# }
5353
# self.storage.set(user_id, serialized_data)
5454

55+
# def _save_to_storage(self, user_id: str):
56+
# """Save session data to storage."""
57+
# session_data = self.user_sessions.get(user_id)
58+
# if session_data:
59+
# # We need to serialize the session data
60+
# # Token manager can't be directly serialized
61+
# serialized_data = {
62+
# "user_info": session_data["user_info"],
63+
# "tokens": session_data["token_manager"].tokens,
64+
# }
65+
# self.storage_manager.set(user_id, serialized_data)
66+
67+
5568
def _save_to_storage(self, user_id: str):
5669
"""Save session data to storage."""
5770
session_data = self.user_sessions.get(user_id)
@@ -62,6 +75,8 @@ def _save_to_storage(self, user_id: str):
6275
"user_info": session_data["user_info"],
6376
"tokens": session_data["token_manager"].tokens,
6477
}
78+
# Store with user: prefix to make it user-specific but device-independent
79+
# if you want device-specific sessions, remove the "user:" prefix
6580
self.storage_manager.set(user_id, serialized_data)
6681

6782
# def _load_from_storage(self, user_id: str) -> bool:
@@ -196,7 +211,8 @@ def logout(self, user_id: str) -> None:
196211

197212
# Delete from storage
198213
# self.storage.delete(user_id)
199-
self.storage_manager.delete(user_id)
214+
# self.storage_manager.delete(user_id)
215+
self.storage_manager.clear_device_data()
200216

201217
def cleanup_expired_sessions(self) -> None:
202218
"""Remove expired sessions from memory and storage."""

kinde_sdk/core/storage/storage_manager.py

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# core/storage/storage_manager.py
22
import threading
3+
import uuid
4+
import time
35
from typing import Dict, Any, Optional
46
from .storage_factory import StorageFactory
57
from .storage_interface import StorageInterface
@@ -22,21 +24,52 @@ def __init__(self):
2224

2325
self._storage = None
2426
self._initialized = True
27+
self._device_id = None
2528

26-
def initialize(self, config: Dict[str, Any] = None):
29+
def initialize(self, config: Dict[str, Any] = None, device_id: Optional[str] = None):
2730
"""
2831
Initialize the storage with the provided configuration.
2932
3033
Args:
3134
config (Dict[str, Any], optional): Configuration dictionary for storage.
3235
If None, defaults to in-memory storage.
36+
device_id (str, optional): A unique identifier for the current device/session.
37+
If None, a random identifier will be generated.
3338
"""
3439
with self._lock:
3540
if config is None:
3641
config = {"type": "memory"}
3742

3843
self._storage = StorageFactory.create_storage(config)
44+
# Set or generate device ID
45+
if device_id:
46+
self._device_id = device_id
47+
elif not self._device_id:
48+
# Generate a persistent device ID if none provided
49+
self._device_id = str(uuid.uuid4())
50+
51+
# Store the device ID in storage for persistence
52+
self._storage.set("_device_id", {"value": self._device_id, "timestamp": time.time()})
3953

54+
def get_device_id(self) -> str:
55+
"""
56+
Get the current device ID.
57+
58+
Returns:
59+
str: The current device ID
60+
"""
61+
if not self._device_id:
62+
# Try to load from storage
63+
stored_device = self.get("_device_id")
64+
if stored_device and "value" in stored_device:
65+
self._device_id = stored_device["value"]
66+
else:
67+
# Generate a new device ID
68+
self._device_id = str(uuid.uuid4())
69+
self.set("_device_id", {"value": self._device_id, "timestamp": time.time()})
70+
71+
return self._device_id
72+
4073
@property
4174
def storage(self) -> Optional[StorageInterface]:
4275
"""
@@ -51,6 +84,34 @@ def storage(self) -> Optional[StorageInterface]:
5184

5285
return self._storage
5386

87+
def _get_namespaced_key(self, key: str) -> str:
88+
"""
89+
Create a namespaced key that includes the device ID to prevent
90+
session clashes between same user on different devices.
91+
92+
Args:
93+
key (str): The original key
94+
95+
Returns:
96+
str: A namespaced key including device ID
97+
"""
98+
device_id = self.get_device_id()
99+
100+
# If the key is for the device ID itself, don't namespace it
101+
if key == "_device_id":
102+
return key
103+
104+
# Special handling for keys that should be global (shared across devices)
105+
if key.startswith("global:"):
106+
return key
107+
108+
# For user-specific but device-independent storage (like OAuth state)
109+
if key.startswith("user:"):
110+
return key
111+
112+
# For device-specific user data (default)
113+
return f"device:{device_id}:{key}"
114+
54115
def get(self, key: str) -> Optional[Dict]:
55116
"""
56117
Retrieve data from storage by key.
@@ -64,7 +125,8 @@ def get(self, key: str) -> Optional[Dict]:
64125
if self._storage is None:
65126
self.initialize()
66127

67-
return self._storage.get(key)
128+
namespaced_key = self._get_namespaced_key(key)
129+
return self._storage.get(namespaced_key)
68130

69131
def set(self, key: str, value: Dict) -> None:
70132
"""
@@ -77,7 +139,9 @@ def set(self, key: str, value: Dict) -> None:
77139
if self._storage is None:
78140
self.initialize()
79141

80-
self._storage.set(key, value)
142+
namespaced_key = self._get_namespaced_key(key)
143+
self._storage.set(namespaced_key, value)
144+
81145

82146
def delete(self, key: str) -> None:
83147
"""
@@ -89,4 +153,34 @@ def delete(self, key: str) -> None:
89153
if self._storage is None:
90154
self.initialize()
91155

92-
self._storage.delete(key)
156+
namespaced_key = self._get_namespaced_key(key)
157+
self._storage.delete(namespaced_key)
158+
159+
def clear_device_data(self) -> None:
160+
"""
161+
Clear all data associated with the current device.
162+
Useful for complete logout/reset scenarios.
163+
164+
Note: This is implementation dependent and works best with
165+
storage backends that support prefix-based operations.
166+
"""
167+
if self._storage is None:
168+
return
169+
170+
device_id = self.get_device_id()
171+
prefix = f"device:{device_id}:"
172+
173+
# This is a naive implementation for storage that doesn't support prefix operations
174+
# For more efficient implementations, extend StorageInterface with prefix operations
175+
176+
# Note: This implementation works with memory storage as a fallback
177+
# but won't be efficient for all storage types
178+
if hasattr(self._storage, "clear_prefix"):
179+
# If the storage implementation supports prefix clearing
180+
self._storage.clear_prefix(prefix)
181+
else:
182+
# Fallback implementation - less efficient
183+
if hasattr(self._storage, "_storage") and isinstance(self._storage._storage, dict):
184+
keys_to_delete = [k for k in self._storage._storage.keys() if k.startswith(prefix)]
185+
for k in keys_to_delete:
186+
self._storage.delete(k)

0 commit comments

Comments
 (0)