Skip to content

Commit 1af9efa

Browse files
authored
Merge pull request #122 from brettchaldecott/fix/fixed-framework-support-for-null-framework
feat: enhance null framework for standalone OAuth usage
2 parents dc5cab4 + 45d1714 commit 1af9efa

File tree

10 files changed

+1816
-17
lines changed

10 files changed

+1816
-17
lines changed

examples/simple_http_oauth_server.py

Lines changed: 560 additions & 0 deletions
Large diffs are not rendered by default.

kinde_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from kinde_sdk.core.framework.framework_factory import FrameworkFactory
2424
from kinde_sdk.core.framework.framework_interface import FrameworkInterface
2525
from kinde_sdk.core.framework.null_framework import NullFramework
26+
from kinde_sdk.core.session_management import KindeSessionManagement
2627

2728
__version__ = "2.1.1"
2829

@@ -42,4 +43,5 @@
4243
"FrameworkFactory",
4344
"FrameworkInterface",
4445
"NullFramework",
46+
"KindeSessionManagement",
4547
]

kinde_sdk/auth/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from .api_options import ApiOptions
55
from .permissions import permissions
66
from .claims import claims
7+
from .async_claims import async_claims
78
from .feature_flags import feature_flags
89
from .portals import portals
910
from .tokens import tokens
1011
from .roles import roles
1112

12-
__all__ = ["OAuth", "TokenManager", "UserSession", "permissions", "ApiOptions", "claims", "feature_flags", "portals", "tokens", "roles"]
13+
__all__ = ["OAuth", "TokenManager", "UserSession", "permissions", "ApiOptions", "claims", "async_claims", "feature_flags", "portals", "tokens", "roles"]

kinde_sdk/auth/oauth.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,8 @@ def __init__(
8080
if framework:
8181
self._initialize_framework()
8282
else:
83-
# Use configuration-based storage if no framework specified
84-
self._storage = StorageFactory.create_storage(storage_config)
85-
self._storage_manager.initialize(config=storage_config, storage=self._storage)
83+
# Use null framework for standalone usage
84+
self._initialize_null_framework(storage_config)
8685

8786
self._session_manager = UserSession()
8887

@@ -124,6 +123,33 @@ def _initialize_framework(self) -> None:
124123
# Initialize storage manager with the framework-specific storage
125124
self._storage_manager.initialize(config={"type": self.framework, "device_id": self._framework.get_name()}, storage=self._storage)
126125

126+
def _initialize_null_framework(self, storage_config: Dict[str, Any]) -> None:
127+
"""
128+
Initialize the null framework for standalone usage.
129+
This sets up the null framework and its associated storage.
130+
"""
131+
from kinde_sdk.core.framework.null_framework import NullFramework
132+
133+
# Create null framework instance (singleton)
134+
self._framework = NullFramework()
135+
136+
# Set the OAuth instance in the framework
137+
self._framework.set_oauth(self)
138+
139+
# Start the framework (no-op for null framework)
140+
self._framework.start()
141+
142+
# Create storage using provided config (defaulting to memory)
143+
storage_type = (storage_config or {}).get("type", "memory") if isinstance(storage_config, dict) else "memory"
144+
self._storage = StorageFactory.create_storage({**({"type": storage_type}), **(storage_config or {})})
145+
146+
# Initialize storage manager with explicit device_id
147+
self._storage_manager.initialize(
148+
config={"type": storage_type},
149+
device_id=self._framework.get_name(),
150+
storage=self._storage,
151+
)
152+
127153
def is_authenticated(self) -> bool:
128154
"""
129155
Check if the user is authenticated using the session manager.
@@ -365,9 +391,6 @@ async def login(self, login_options: Dict[str, Any] = None) -> str:
365391
Returns:
366392
Login URL
367393
"""
368-
if not self.framework:
369-
raise KindeConfigurationException("Framework must be selected")
370-
371394
if login_options is None:
372395
login_options = {}
373396

@@ -407,9 +430,6 @@ async def register(self, login_options: Dict[str, Any] = None) -> str:
407430
Returns:
408431
Registration URL
409432
"""
410-
if not self.framework:
411-
raise KindeConfigurationException("Framework must be selected")
412-
413433
if login_options is None:
414434
login_options = {}
415435

@@ -440,9 +460,6 @@ async def logout(self, user_id: Optional[str] = None, logout_options: Dict[str,
440460
Returns:
441461
Logout URL
442462
"""
443-
if not self.framework:
444-
raise KindeConfigurationException("Framework must be selected")
445-
446463
if logout_options is None:
447464
logout_options = {}
448465

kinde_sdk/core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Import helpers for easier access from outside the package
22
from .storage import StorageInterface, StorageFactory, StorageManager
33
from .framework import FrameworkInterface, FrameworkFactory, NullFramework
4+
from .session_management import KindeSessionManagement
45

56
__all__ = [
67
'StorageInterface',
@@ -9,4 +10,5 @@
910
'FrameworkInterface',
1011
'FrameworkFactory',
1112
'NullFramework',
13+
'KindeSessionManagement',
1214
]

kinde_sdk/core/framework/null_framework.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from typing import Optional, Any, TYPE_CHECKING
2+
import threading
3+
import contextvars
24

35
if TYPE_CHECKING:
46
from kinde_sdk.auth.oauth import OAuth
@@ -8,12 +10,28 @@
810
class NullFramework(FrameworkInterface):
911
"""
1012
A null implementation of the FrameworkInterface.
11-
This implementation does nothing and is used when no framework is detected or specified.
13+
This implementation provides session management for standalone usage without a web framework.
14+
Uses a singleton pattern to allow external applications to set the current user session.
1215
"""
1316

17+
_instance = None
18+
_lock = threading.Lock()
19+
20+
def __new__(cls):
21+
"""Implement singleton pattern."""
22+
if cls._instance is None:
23+
with cls._lock:
24+
if cls._instance is None:
25+
cls._instance = super(NullFramework, cls).__new__(cls)
26+
return cls._instance
27+
1428
def __init__(self):
1529
"""Initialize the null framework."""
16-
self._oauth = None
30+
if not hasattr(self, '_initialized'):
31+
self._oauth = None
32+
# Use context variables for user_id to avoid race conditions in both threads and async
33+
self._current_user_id_context = contextvars.ContextVar('current_user_id', default=None)
34+
self._initialized = True
1735

1836
def get_name(self) -> str:
1937
"""
@@ -31,7 +49,7 @@ def get_description(self) -> str:
3149
Returns:
3250
str: A description of the null framework
3351
"""
34-
return "A null framework implementation that does nothing. Used when no framework is detected or specified."
52+
return "A null framework implementation that provides session management for standalone usage without a web framework."
3553

3654
def start(self) -> None:
3755
"""
@@ -65,11 +83,46 @@ def get_request(self) -> Optional[Any]:
6583
"""
6684
return None
6785

86+
def get_user_id(self) -> Optional[str]:
87+
"""
88+
Get the current user ID from the session.
89+
90+
Returns:
91+
Optional[str]: The current user ID, or None if not set
92+
"""
93+
return self._current_user_id_context.get()
94+
95+
def set_user_id(self, user_id: str) -> None:
96+
"""
97+
Set the current user ID for the session.
98+
This method allows external applications (like simple HTTP servers)
99+
to set the current user session.
100+
101+
Args:
102+
user_id (str): The user ID to set as current
103+
"""
104+
self._current_user_id_context.set(user_id)
105+
106+
def clear_user_id(self) -> None:
107+
"""
108+
Clear the current user ID from the session.
109+
"""
110+
self._current_user_id_context.set(None)
111+
68112
def set_oauth(self, oauth: 'OAuth') -> None:
69113
"""
70114
Set the OAuth instance for this framework.
71115
72116
Args:
73117
oauth (OAuth): The OAuth instance
74118
"""
75-
self._oauth = oauth
119+
self._oauth = oauth
120+
121+
def can_auto_detect(self) -> bool:
122+
"""
123+
Check if this framework can be auto-detected.
124+
125+
Returns:
126+
bool: False - null framework is not auto-detected
127+
"""
128+
return False
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
Kinde Session Management
3+
4+
This module provides a user-friendly interface for session management when using
5+
the Kinde SDK in standalone mode (without a web framework). It wraps access to
6+
the NullFramework behind a more intuitive API.
7+
"""
8+
9+
from typing import Optional, TYPE_CHECKING
10+
import logging
11+
12+
if TYPE_CHECKING:
13+
from kinde_sdk.core.framework.null_framework import NullFramework
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class KindeSessionManagement:
19+
"""
20+
A user-friendly interface for session management in standalone Kinde SDK usage.
21+
22+
This class provides a clean API for managing user sessions when using the Kinde SDK
23+
without a web framework. It wraps the NullFramework functionality behind a more
24+
intuitive interface.
25+
26+
Note: This class can only be used when the SDK is running in standalone mode
27+
(without a web framework). If a framework like FastAPI or Flask is being used,
28+
this class will raise an exception to prevent misuse.
29+
"""
30+
31+
def __init__(self):
32+
"""
33+
Initialize the Kinde session management.
34+
35+
Raises:
36+
RuntimeError: If the SDK is not running in standalone mode (NullFramework not active)
37+
"""
38+
self._logger = logging.getLogger(__name__)
39+
self._null_framework = self._get_null_framework()
40+
41+
if not self._null_framework:
42+
raise RuntimeError(
43+
"KindeSessionManagement can only be used in standalone mode. "
44+
"When using a web framework (FastAPI, Flask, etc.), session management "
45+
"is handled automatically by the framework. Please use the framework's "
46+
"built-in session management instead."
47+
)
48+
49+
def _get_null_framework(self) -> Optional['NullFramework']:
50+
"""
51+
Get the current NullFramework instance if it's active.
52+
53+
Returns:
54+
Optional[NullFramework]: The NullFramework instance if active, None otherwise
55+
"""
56+
try:
57+
from kinde_sdk.core.framework.null_framework import NullFramework
58+
from kinde_sdk.core.framework.framework_factory import FrameworkFactory
59+
60+
# First, try to get the framework instance from FrameworkFactory
61+
current_framework = FrameworkFactory.get_framework_instance()
62+
63+
# Check if it's a NullFramework instance
64+
if current_framework and isinstance(current_framework, NullFramework):
65+
return current_framework
66+
67+
# If not found in FrameworkFactory, try to get it from the NullFramework singleton
68+
# This handles the case where OAuth creates NullFramework directly
69+
try:
70+
null_framework = NullFramework()
71+
# Check if this is actually being used (has been initialized)
72+
if hasattr(null_framework, '_initialized') and null_framework._initialized:
73+
return null_framework
74+
except Exception:
75+
pass
76+
77+
return None
78+
79+
except Exception as e:
80+
self._logger.debug(f"Could not get NullFramework: {e}")
81+
return None
82+
83+
def get_user_id(self) -> Optional[str]:
84+
"""
85+
Get the current user ID from the session.
86+
87+
Returns:
88+
Optional[str]: The current user ID, or None if not set
89+
"""
90+
if not self._null_framework:
91+
raise RuntimeError("Session management is not available in this context")
92+
93+
return self._null_framework.get_user_id()
94+
95+
def set_user_id(self, user_id: str) -> None:
96+
"""
97+
Set the current user ID for the session.
98+
99+
This method allows you to set the current user session, which is useful
100+
for applications that need to manage multiple user sessions or when
101+
integrating with custom session management systems.
102+
103+
Args:
104+
user_id (str): The user ID to set as current
105+
106+
Raises:
107+
RuntimeError: If session management is not available in this context
108+
ValueError: If user_id is empty or None
109+
"""
110+
if not self._null_framework:
111+
raise RuntimeError("Session management is not available in this context")
112+
113+
if not user_id or not user_id.strip():
114+
raise ValueError("user_id cannot be empty or None")
115+
116+
self._null_framework.set_user_id(user_id)
117+
self._logger.debug(f"Set user ID: {user_id}")
118+
119+
def clear_user_id(self) -> None:
120+
"""
121+
Clear the current user ID from the session.
122+
123+
This method removes the current user session, effectively logging out
124+
the user from the current context.
125+
126+
Raises:
127+
RuntimeError: If session management is not available in this context
128+
"""
129+
if not self._null_framework:
130+
raise RuntimeError("Session management is not available in this context")
131+
132+
self._null_framework.clear_user_id()
133+
self._logger.debug("Cleared user ID from session")
134+
135+
def is_user_logged_in(self) -> bool:
136+
"""
137+
Check if a user is currently logged in.
138+
139+
Returns:
140+
bool: True if a user is logged in, False otherwise
141+
"""
142+
if not self._null_framework:
143+
raise RuntimeError("Session management is not available in this context")
144+
145+
user_id = self._null_framework.get_user_id()
146+
return user_id is not None and user_id.strip() != ""
147+
148+
def get_session_info(self) -> dict:
149+
"""
150+
Get information about the current session.
151+
152+
Returns:
153+
dict: A dictionary containing session information
154+
155+
Raises:
156+
RuntimeError: If session management is not available in this context
157+
"""
158+
if not self._null_framework:
159+
raise RuntimeError("Session management is not available in this context")
160+
161+
user_id = self._null_framework.get_user_id()
162+
163+
return {
164+
"user_id": user_id,
165+
"is_logged_in": user_id is not None and user_id.strip() != "",
166+
"framework": "standalone",
167+
"session_type": "null_framework"
168+
}
169+
170+
def __repr__(self) -> str:
171+
"""Return a string representation of the session management object."""
172+
if not self._null_framework:
173+
return "KindeSessionManagement(unavailable - not in standalone mode)"
174+
175+
user_id = self._null_framework.get_user_id()
176+
status = "logged_in" if user_id else "not_logged_in"
177+
return f"KindeSessionManagement(user_id={user_id}, status={status})"

0 commit comments

Comments
 (0)