kinde-python-sdk 2.0.7__py3-none-any.whl → 2.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,8 @@ import logging
9
9
  from kinde_sdk.auth.oauth import OAuth
10
10
  from kinde_sdk.auth import claims, feature_flags, permissions, tokens
11
11
  from kinde_sdk.management import ManagementClient;
12
+ from kinde_sdk.management.management_token_manager import ManagementTokenManager
13
+ import requests
12
14
 
13
15
  logger = logging.getLogger(__name__)
14
16
 
@@ -78,6 +80,7 @@ async def home(request: Request):
78
80
  <p>tokens: {tokens.get_token_manager().get_access_token()}</p>
79
81
  <p>users: {user_count} user(s) found</p>
80
82
  <p>You are logged in.</p>
83
+ <a href="/call_management_users">Call Management Users</a>
81
84
  <a href="/logout">Logout</a>
82
85
  </body>
83
86
  </html>
@@ -92,6 +95,36 @@ async def home(request: Request):
92
95
  </html>
93
96
  """
94
97
 
98
+ @app.get("/call_management_users")
99
+ async def call_management_users():
100
+ if not kinde_oauth.is_authenticated():
101
+ return {"error": "Not authenticated"}
102
+
103
+ domain = os.getenv("KINDE_DOMAIN")
104
+ client_id = os.getenv("KINDE_MANAGEMENT_CLIENT_ID")
105
+ client_secret = os.getenv("KINDE_MANAGEMENT_CLIENT_SECRET")
106
+
107
+ if not all([domain, client_id, client_secret]):
108
+ return {"error": "Missing management credentials"}
109
+
110
+ try:
111
+ token_manager = ManagementTokenManager(
112
+ domain=domain,
113
+ client_id=client_id,
114
+ client_secret=client_secret
115
+ )
116
+ access_token = token_manager.get_access_token()
117
+
118
+ headers = {
119
+ "Authorization": f"Bearer {access_token}"
120
+ }
121
+ response = requests.get("http://localhost:8000/management/users", headers=headers)
122
+ response.raise_for_status()
123
+ return response.json()
124
+ except Exception as e:
125
+ logger.error(f"Failed to call management users: {e}")
126
+ return {"error": str(e)}
127
+
95
128
  if __name__ == "__main__":
96
129
  import uvicorn
97
130
  uvicorn.run(app, host="127.0.0.1", port=5000)
@@ -0,0 +1,119 @@
1
+ from fastapi import FastAPI, Depends, HTTPException, status
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ import os
4
+ import requests
5
+ from dotenv import load_dotenv
6
+ from kinde_sdk.management.management_token_manager import ManagementTokenManager
7
+ from kinde_sdk.management import ManagementClient
8
+ from typing import Dict, Any
9
+ import time
10
+
11
+ import logging
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ # Setup logging
17
+ logging.basicConfig(level=logging.DEBUG)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Initialize FastAPI app
21
+ app = FastAPI(title="Kinde Management Token Example with Introspection")
22
+
23
+ # Security scheme for bearer token
24
+ security = HTTPBearer()
25
+
26
+ # Extract, introspect, and validate management token from header
27
+ def get_management_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> ManagementTokenManager:
28
+ logger.debug("Starting token validation")
29
+ bearer_token = credentials.credentials
30
+ logger.debug(f"Received bearer token (first 20 chars): {bearer_token[:20]}...")
31
+
32
+ # SDK config from env
33
+ domain = os.getenv("KINDE_HOST", "https://app.kinde.com")
34
+ logger.debug(f"Raw domain from env: {domain}")
35
+ if domain.startswith(('http://', 'https://')):
36
+ domain = domain.split('://', 1)[1]
37
+ logger.debug(f"Normalized domain: {domain}")
38
+ client_id = os.getenv("KINDE_MANAGEMENT_CLIENT_ID")
39
+ client_secret = os.getenv("KINDE_MANAGEMENT_CLIENT_SECRET")
40
+ logger.debug(f"Client ID: {client_id}")
41
+ # Not logging secret for security
42
+
43
+ if not all([domain, client_id, client_secret]):
44
+ logger.error("Missing Kinde management credentials")
45
+ raise HTTPException(
46
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
47
+ detail="Missing Kinde management credentials in environment"
48
+ )
49
+
50
+ try:
51
+ token_manager = ManagementTokenManager(
52
+ domain=domain,
53
+ client_id=client_id,
54
+ client_secret=client_secret
55
+ )
56
+ logger.debug(f"ManagementTokenManager instantiated {bearer_token}")
57
+
58
+ introspection_result = token_manager.validate_and_set_via_introspection(bearer_token)
59
+ logger.debug(f"Introspection result: {introspection_result}")
60
+
61
+ access_token = token_manager.get_access_token()
62
+ if not access_token:
63
+ logger.error("No access token after introspection")
64
+ raise ValueError("Invalid management token after introspection")
65
+ logger.debug("Access token obtained successfully")
66
+
67
+ return token_manager
68
+
69
+ except ValueError as e:
70
+ logger.error(f"ValueError in token validation: {str(e)}")
71
+ raise HTTPException(
72
+ status_code=status.HTTP_401_UNAUTHORIZED,
73
+ detail=str(e),
74
+ headers={"WWW-Authenticate": "Bearer"}
75
+ )
76
+ except Exception as e:
77
+ logger.error(f"Exception in token validation: {str(e)}", exc_info=True)
78
+ raise HTTPException(
79
+ status_code=status.HTTP_401_UNAUTHORIZED,
80
+ detail=f"Token introspection failed: {str(e)}",
81
+ headers={"WWW-Authenticate": "Bearer"}
82
+ )
83
+
84
+ # Example route using the validated management token
85
+ @app.get("/management/users")
86
+ async def get_users(token_manager: ManagementTokenManager = Depends(get_management_token)):
87
+ logger.debug("Entering get_users endpoint")
88
+ try:
89
+ # Create ManagementClient with the token manager
90
+ management_client = ManagementClient(
91
+ domain=token_manager.domain,
92
+ client_id=token_manager.client_id,
93
+ client_secret=token_manager.client_secret
94
+ )
95
+ logger.debug("ManagementClient created")
96
+
97
+ # Fetch users (example API call)
98
+ users_response = management_client.get_users()
99
+
100
+ # Get the user count from the response
101
+ user_count = len(users_response.users) if users_response.users else 0
102
+ logger.debug(f"Fetched {user_count} users")
103
+
104
+ return {
105
+ "message": "Users fetched successfully",
106
+ "user_count": user_count,
107
+ "users": users_response.users if users_response.users else [] # In production, filter sensitive data
108
+ }
109
+ except Exception as e:
110
+ logger.error(f"Error in get_users: {str(e)}", exc_info=True)
111
+ raise HTTPException(
112
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
113
+ detail=f"Failed to fetch users: {str(e)}"
114
+ ) from e
115
+
116
+ # Run the app
117
+ if __name__ == "__main__":
118
+ import uvicorn
119
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinde-python-sdk
3
- Version: 2.0.7
3
+ Version: 2.0.9
4
4
  Summary: Connect your app to the Kinde platform
5
5
  Author-email: Kinde Engineering <engineering@kinde.com>
6
6
  Project-URL: Homepage, https://github.com/kinde-oss/kinde-python-sdk
@@ -1,5 +1,6 @@
1
1
  kinde_fastapi/__init__.py,sha256=FzB0zDBbzaLLvGZBPrBZN94I6LRZdkT4RSmGUsbh3mA,470
2
- kinde_fastapi/examples/example_app.py,sha256=OVJDFKzT3Qv_tA4GgizW2DmjoE0hm_9O4H_DKIGYNeQ,3057
2
+ kinde_fastapi/examples/example_app.py,sha256=c29SsAytpDs75CVJuiaInLK8lnqMLmoDDvExtRI-SYM,4270
3
+ kinde_fastapi/examples/management_token_example.py,sha256=keO-eKBrrMUTcIom154Mmn7BLRSZ45jYvQPwT-Jeoqw,4580
3
4
  kinde_fastapi/examples/session.py,sha256=WblGM7TM2JCKdSFsd2OmSP0daexZ7E6ZS1xgTQY_etY,2472
4
5
  kinde_fastapi/framework/__init__.py,sha256=tpTI84q7HCHMlREslMrngIlKwC8CDog9bEMI_ydhCUs,181
5
6
  kinde_fastapi/framework/fastapi_framework.py,sha256=-MiBFddfY3NiB9BE_b8BBYAxfGbhOQKO-RXUsCu4a3U,6767
@@ -13,7 +14,7 @@ kinde_flask/framework/flask_framework.py,sha256=I4dVWmkyd0FBqjAkOyftebm7_qpI9uxk
13
14
  kinde_flask/framework/flask_framework_factory.py,sha256=ALpsj-cHBox3ktRxi-CyZ5Q6EOvacfdu4pvlTYkuviA,802
14
15
  kinde_flask/middleware/framework_middleware.py,sha256=XFK6kgMDuHkrawDJ0_MMgLO7es0YQVjlVDDRDgwlCJ0,1012
15
16
  kinde_flask/storage/flask_storage_factory.py,sha256=MitOUbfCbfvSQFFoUbbnbXLEOL_qajS29Mlsn0TyTsY,2562
16
- kinde_python_sdk-2.0.7.dist-info/licenses/LICENSE,sha256=iT6AIO6NJn_mo0kDD5mpz2zp9GpzH6YdhqOmkCBg-kQ,1385
17
+ kinde_python_sdk-2.0.9.dist-info/licenses/LICENSE,sha256=iT6AIO6NJn_mo0kDD5mpz2zp9GpzH6YdhqOmkCBg-kQ,1385
17
18
  kinde_sdk/__init__.py,sha256=Mh3eVcnJDk-KdQfxVUsF19NgBYexoJCwG5iAbD1LEdQ,1052
18
19
  kinde_sdk/api_client.py,sha256=GEb1BIjvIrUe8mFIr7hppp2PO604BiSlAr0_TYBfuy4,58496
19
20
  kinde_sdk/configuration.py,sha256=oqZGvKEf4kNNURbI1pip2ZVVcDyRTSmmnjXWziH4s80,15765
@@ -109,7 +110,7 @@ kinde_sdk/apis/tags/users_api.py,sha256=06qeWo8ZNNZvKQ5hl0ImtPvNSbzDKkceZj7ACrlu
109
110
  kinde_sdk/apis/tags/webhooks_api.py,sha256=AmLDLeW6aLz3lnqUGGd2dpVxuVKxW1KgIW2rdtUNJmw,925
110
111
  kinde_sdk/auth/__init__.py,sha256=czQs-NJ3R22EXkcnszl3YJKmjsB-mVpJ4T11zbRb_4Y,379
111
112
  kinde_sdk/auth/base_auth.py,sha256=hV-xjaGa35YMDFpmEWQgTNM7w6ZHskLm_p5cnXSSLJY,1253
112
- kinde_sdk/auth/claims.py,sha256=g0vlQtHBNyuELCNSTuahr3ZhBma4Htp2KjKCtKq1f90,1540
113
+ kinde_sdk/auth/claims.py,sha256=sS4cvtEYQyTlT4WacibyKzp-5eTTH4JbMnDabMTX31w,1560
113
114
  kinde_sdk/auth/config_loader.py,sha256=L01kqmzUMKQ0X_PA9MYcWSeJiVoDfrB4RANQGWS76UA,879
114
115
  kinde_sdk/auth/enums.py,sha256=Xbk7jwXtq_TViZSfBy6h9lon8VEA5v6ssUl-3yKp8Zk,538
115
116
  kinde_sdk/auth/feature_flags.py,sha256=VSqfyqNmYLo3Q74nyo4L8W8jt8msZ_v-45Y7pW_48VA,3810
@@ -117,7 +118,7 @@ kinde_sdk/auth/login_options.py,sha256=3Fvbo-rjLzbV14Np48Ogw0HieThjlRtfP4VHztJtG
117
118
  kinde_sdk/auth/oauth.py,sha256=bXWM6Mo7H8EgrD2GSOfhibvwz6MVMPe7-wM1PaL6_6o,26136
118
119
  kinde_sdk/auth/permissions.py,sha256=A9BmGt3OokuvS-4twveCZdCPFY-QobyzqlgGVo8eSoI,1913
119
120
  kinde_sdk/auth/portals.py,sha256=Z3goLvio-Xw7VjTRrQZXKOFkTMvtF78nKzFF5-u4rkY,4530
120
- kinde_sdk/auth/token_manager.py,sha256=uG9TFcJ_K4hbgFEP9gy_S1xRUBpzD5ZYT37LzMWoWHA,7182
121
+ kinde_sdk/auth/token_manager.py,sha256=HSfyKmc4kyoQz2NeRPVWtpY4E4rtf2w6SWArRlQ7-5s,8278
121
122
  kinde_sdk/auth/tokens.py,sha256=eZKJTTw764FrzvxurWNee1_ZX1Q2FQjJUXBlsMlp2A0,2367
122
123
  kinde_sdk/auth/user_session.py,sha256=CmFotC9ONzHwPMRJy4SOT7tOI_63dHv_ttpdNHssH8Y,7657
123
124
  kinde_sdk/core/__init__.py,sha256=ejO0P_oXuiO5V-MJVRqKu6y0fAWKyi4iL2UIRGXhLy0,358
@@ -141,7 +142,7 @@ kinde_sdk/management/api_response.py,sha256=eMxw1mpmJcoGZ3gs9z6jM4oYoZ10Gjk333s9
141
142
  kinde_sdk/management/configuration.py,sha256=icAfQBYmokmspDa_ELha2Qfeq_IkgAp6COdmw1ZpL2Q,18909
142
143
  kinde_sdk/management/exceptions.py,sha256=I_xzvCXjHszPUrUr4dNsE9Pg-nH58YszF075NvjSRMA,6780
143
144
  kinde_sdk/management/management_client.py,sha256=_GErLV96nTbWw58fscCrDe_UOVk25u4Vlx0amFp3g6Y,31467
144
- kinde_sdk/management/management_token_manager.py,sha256=uEYQBiN5mNqZujTSYcU2sl2Sk51YwSe9_b-XkDq4uAI,9853
145
+ kinde_sdk/management/management_token_manager.py,sha256=KP1UVjTFdjJlLMRYn2fzJOS2QgEuaQaTg56EmQs22uE,12106
145
146
  kinde_sdk/management/rest.py,sha256=7kMCS7NAOYCI4hrThABWpV-AN0Y-LuCjMnJ404CJdq8,9783
146
147
  kinde_sdk/management/schemas.py,sha256=6tg1tEh8IL-9UuZiWFVe-0Pu-TD-CYW37soeQ6AyspA,97671
147
148
  kinde_sdk/management/api/__init__.py,sha256=-LP_IUIY_BztruZ5aTT7WoTgolTlSxOgppthL4I2l8A,2018
@@ -722,7 +723,7 @@ kinde_sdk/test/test_models/test_user_profile_v2.py,sha256=QzYjvjtTGa1ktE5ruyGBm_
722
723
  kinde_sdk/test/test_models/test_users.py,sha256=wJQR5K7dsfHQQsOQp4mrHWQynFPHwL-gDtbgVifNfMs,537
723
724
  kinde_sdk/test/test_models/test_users_response.py,sha256=BCcKxe9GctDrOXOzw0Q7O1ijKy_93Oj3E2wHW00y624,869
724
725
  kinde_sdk/test/test_models/test_webhook.py,sha256=KNfR8sKAK5H2vX_cXYGT5EfgaVP7_egC39uY6-s-4qo,544
725
- kinde_python_sdk-2.0.7.dist-info/METADATA,sha256=hfaFwDNTTOdeJhG8N1f_T2cwvtq9UP_5pee2R_VZXvw,23482
726
- kinde_python_sdk-2.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
727
- kinde_python_sdk-2.0.7.dist-info/top_level.txt,sha256=TUU3EVjV6O4brF-mooQr6pnTk-jXJphmIWRHCIdyIgI,36
728
- kinde_python_sdk-2.0.7.dist-info/RECORD,,
726
+ kinde_python_sdk-2.0.9.dist-info/METADATA,sha256=-T2f9gM_AfnugRp4TestVW7eWIeB3Te9rsXRr57CNDs,23482
727
+ kinde_python_sdk-2.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
728
+ kinde_python_sdk-2.0.9.dist-info/top_level.txt,sha256=TUU3EVjV6O4brF-mooQr6pnTk-jXJphmIWRHCIdyIgI,36
729
+ kinde_python_sdk-2.0.9.dist-info/RECORD,,
kinde_sdk/auth/claims.py CHANGED
@@ -24,7 +24,7 @@ class Claims(BaseAuth):
24
24
  "value": None
25
25
  }
26
26
 
27
- claims = token_manager.get_claims()
27
+ claims = token_manager.get_claims(token_type)
28
28
  value = claims.get(claim_name)
29
29
 
30
30
  return {
@@ -46,7 +46,7 @@ class Claims(BaseAuth):
46
46
  if not token_manager:
47
47
  return {}
48
48
 
49
- return token_manager.get_claims()
49
+ return token_manager.get_claims(token_type)
50
50
 
51
51
  # Create a singleton instance
52
52
  claims = Claims()
@@ -153,17 +153,46 @@ class TokenManager:
153
153
  """Get the ID token if available."""
154
154
  return self.tokens.get("id_token")
155
155
 
156
- def get_claims(self):
157
- """Get the claims from the access token if available, falling back to ID token claims."""
158
- # First try to get claims from access token
159
- claims = self.tokens.get("access_token_claims", {})
156
+ def get_claims(self, token_type: str = "access_token"):
157
+ """Get the claims from the specified token type.
158
+
159
+ Args:
160
+ token_type (str): The type of token to get claims from.
161
+ Valid values are "access_token" or "id_token".
162
+
163
+ Returns:
164
+ dict: The claims from the specified token, or empty dict if not available.
165
+ """
166
+ # Validate token type
167
+ valid_token_types = ["access_token", "id_token"]
168
+ if token_type not in valid_token_types:
169
+ logging.warning(f"Invalid token_type '{token_type}'. Valid types are: {valid_token_types}")
170
+ return {}
171
+
172
+ # Use f-string for safer string formatting
173
+ claims_key = f"{token_type}_claims"
174
+ claims = self.tokens.get(claims_key, {})
175
+
160
176
  if not claims:
161
- # Fall back to ID token claims if access token claims are not available
162
- claims = self.tokens.get("id_token_claims", {})
163
- if not claims:
164
- logging.warning("No claims available in token manager")
177
+ logging.warning(f"No claims available for token type: {token_type}")
178
+
165
179
  return claims
166
180
 
181
+ def get_claim(self, key: str, token_type: str = "access_token"):
182
+ """Get a specific claim from the specified token type.
183
+
184
+ Args:
185
+ key (str): The claim key to retrieve
186
+ token_type (str): The type of token to get the claim from.
187
+ Valid values are "access_token" or "id_token".
188
+
189
+ Returns:
190
+ dict: Dictionary containing the claim name and value, or empty dict if not found.
191
+ """
192
+ claims = self.get_claims(token_type)
193
+ value = claims.get(key)
194
+ return {"name": key, "value": value}
195
+
167
196
  def revoke_token(self):
168
197
  """ Revoke the current access token. """
169
198
  if "access_token" not in self.tokens:
@@ -209,13 +209,15 @@ class ManagementTokenManager:
209
209
  def set_tokens(self, token_data: Dict[str, Any]):
210
210
  """ Store tokens with expiration. """
211
211
  with self.lock:
212
- # Handle None values by using default
213
- expires_in = token_data.get("expires_in") or 3600
212
+ # Handle None values by using default, but allow 0
213
+ expires_in = token_data.get("expires_in")
214
+ if expires_in is None:
215
+ expires_in = 3600
214
216
  token_type = token_data.get("token_type") or "Bearer"
215
217
  self.tokens = {
216
- "access_token": token_data.get("access_token"),
217
- "expires_at": time.time() + expires_in,
218
- "token_type": token_type
218
+ "access_token": token_data.get("access_token"),
219
+ "expires_at": time.time() + expires_in,
220
+ "token_type": token_type
219
221
  }
220
222
 
221
223
  def get_access_token(self):
@@ -265,9 +267,64 @@ class ManagementTokenManager:
265
267
  except requests.exceptions.Timeout:
266
268
  raise Exception(f"Token request timed out after 30 seconds for domain {self.domain}")
267
269
  except requests.exceptions.RequestException as e:
268
- raise Exception(f"Token request failed for domain {self.domain}: {str(e)}")
270
+ raise Exception(f"Token request failed for domain {self.domain}: {str(e)}") from e
269
271
 
270
272
  def clear_tokens(self):
271
273
  """ Clear stored tokens. """
272
274
  with self.lock:
273
- self.tokens = {}
275
+ self.tokens = {}
276
+
277
+ def validate_and_set_via_introspection(self, bearer_token: str) -> Dict[str, Any]:
278
+ """
279
+ Validate a bearer token via introspection and set it if valid.
280
+
281
+ Args:
282
+ bearer_token: The bearer token to introspect and validate.
283
+
284
+ Returns:
285
+ Dict: The introspection result if valid.
286
+
287
+ Raises:
288
+ Exception: If introspection fails or token is invalid.
289
+ """
290
+ # First, get a management token using client credentials
291
+ management_token = self.get_access_token()
292
+
293
+ introspection_url = f"https://{self.domain}/oauth2/introspect"
294
+ introspect_data = {
295
+ "token": bearer_token
296
+ }
297
+
298
+ try:
299
+ response = requests.post(
300
+ introspection_url,
301
+ data=introspect_data,
302
+ headers={
303
+ "Content-Type": "application/x-www-form-urlencoded",
304
+ "Authorization": f"Bearer {management_token}",
305
+ "Kinde-SDK": self._generate_tracking_header()
306
+ },
307
+ timeout=30
308
+ )
309
+ response.raise_for_status()
310
+ introspection_result = response.json()
311
+
312
+ if not introspection_result.get("active", False):
313
+ raise ValueError("Token is inactive or invalid")
314
+
315
+ # Set the validated token - calculate proper expiration
316
+ exp_time = introspection_result.get("exp")
317
+ expires_in = max(0, exp_time - int(time.time())) if exp_time else 3600
318
+
319
+ token_data = {
320
+ "access_token": bearer_token,
321
+ "expires_in": expires_in
322
+ }
323
+ self.set_tokens(token_data)
324
+
325
+ return introspection_result
326
+
327
+ except requests.exceptions.Timeout:
328
+ raise Exception(f"Introspection request timed out after 30 seconds for domain {self.domain}") from None
329
+ except requests.exceptions.RequestException as e:
330
+ raise Exception(f"Introspection request failed for domain {self.domain}: {str(e)}") from e