Skip to content

Commit 4cde9e8

Browse files
author
Brett Chaldecott
committed
feat: added multi environment support
1 parent d41bfb0 commit 4cde9e8

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

kinde_sdk/kinde_api_client.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ def is_authenticated(self) -> bool:
133133
self._refresh_token()
134134
return True
135135
return False
136+
137+
def is_authenticated_token(self, token_value: dict) -> dict:
138+
if token_value:
139+
if token_value.is_expired():
140+
return self._refresh_token_value(token_value)
141+
return None
136142

137143
def create_org(self) -> str:
138144
return f"{self.registration_url}&is_create_org=true"
@@ -145,23 +151,50 @@ def get_claim(self, key: str, token_name: str = "access_token") -> Any:
145151
self._decode_token_if_needed(token_name)
146152
value = self.__decoded_tokens[token_name].get(key)
147153
return {"name": key, "value": value}
154+
155+
def get_claim_token(self, token_value: dict, key: str, token_name: str = "access_token") -> Any:
156+
if token_name not in self.TOKEN_NAMES:
157+
raise KindeTokenException(
158+
f"Please use only tokens from the list: {self.TOKEN_NAMES}"
159+
)
160+
161+
decoded_tokens = self._decode_token_if_needed_value(token_name,token_value)
162+
value = decoded_tokens[token_name].get(key)
163+
return {"name": key, "value": value}
148164

149165
def get_permission(self, permission: str) -> Dict[str, Any]:
150166
return {
151167
"org_code": self.get_claim("org_code")["value"],
152168
"is_granted": permission in self.get_claim("permissions")["value"],
153169
}
170+
171+
def get_permission_token(self, token_value: dict, permission: str) -> Dict[str, Any]:
172+
return {
173+
"org_code": self.get_claim_token(token_value, "org_code")["value"],
174+
"is_granted": permission in self.get_claim_token(token_value, "permissions")["value"],
175+
}
154176

155177
def get_permissions(self) -> Dict[str, Any]:
156178
return {
157179
"org_code": self.get_claim("org_code")["value"],
158180
"permissions": self.get_claim("permissions")["value"],
159181
}
182+
183+
def get_permissions_token(self, token_value: dict) -> Dict[str, Any]:
184+
return {
185+
"org_code": self.get_claim_token(token_value, "org_code")["value"],
186+
"permissions": self.get_claim_token(token_value, "permissions")["value"],
187+
}
160188

161189
def get_organization(self) -> Dict[str, str]:
162190
return {
163191
"org_code": self.get_claim("org_code")["value"],
164192
}
193+
194+
def get_organization_token(self, token_value: dict) -> Dict[str, str]:
195+
return {
196+
"org_code": self.get_claim_token(token_value, "org_code")["value"],
197+
}
165198

166199
def get_user_details(self) -> Dict[str, str]:
167200
return {
@@ -171,11 +204,25 @@ def get_user_details(self) -> Dict[str, str]:
171204
"email": self.get_claim("email", "id_token")["value"],
172205
"picture": self.get_claim("picture", "id_token")["value"],
173206
}
207+
208+
def get_user_details_token(self,token_value: dict) -> Dict[str, str]:
209+
return {
210+
"id": self.get_claim_token(token_value, "sub","id_token")["value"],
211+
"given_name": self.get_claim_token(token_value, "given_name", "id_token")["value"],
212+
"family_name": self.get_claim_token(token_value, "family_name", "id_token")["value"],
213+
"email": self.get_claim_token(token_value, "email", "id_token")["value"],
214+
"picture": self.get_claim_token(token_value, "picture", "id_token")["value"],
215+
}
174216

175217
def get_user_organizations(self) -> Dict[str, List[str]]:
176218
return {
177219
"org_codes": self.get_claim("org_codes", "id_token")["value"],
178220
}
221+
222+
def get_user_organizations_token(self, token_value: dict) -> Dict[str, List[str]]:
223+
return {
224+
"org_codes": self.get_claim_token(token_value, "org_codes", "id_token")["value"],
225+
}
179226

180227
def get_flag(
181228
self, code: str, default_value: Any = None, flag_type: str = ""
@@ -205,15 +252,53 @@ def get_flag(
205252
result_flag["type"] = FlagType[flag_type].value
206253

207254
return result_flag
255+
256+
def get_flag_token(
257+
self, token_value: dict, code: str, default_value: Any = None, flag_type: str = ""
258+
) -> Any:
259+
flags = self.get_claim_token(token_value, "feature_flags")["value"] or {}
260+
flag = {}
261+
262+
if code not in list(flags.keys()):
263+
if default_value is None:
264+
raise KindeRetrieveException(
265+
f"Flag {code} was not found, and no default value has been provided"
266+
)
267+
else:
268+
flag = flags[code]
269+
if flag_type and flag.get("t") and flag_type != flag.get("t"):
270+
raise KindeRetrieveException(
271+
f"Flag {code} is of type {FlagType[flag.get('t')].value} - requested type {FlagType[flag_type].value}"
272+
)
273+
274+
result_flag = {
275+
"code": code,
276+
"value": flag.get("v") if flag else default_value,
277+
"is_default": not bool(flag),
278+
}
279+
flag_type = flag["t"] if flag else flag_type
280+
if flag_type:
281+
result_flag["type"] = FlagType[flag_type].value
282+
283+
return result_flag
208284

209285
def get_boolean_flag(self, code: str, default_value: Any = None) -> bool:
210286
return self.get_flag(code, default_value, "b")["value"]
287+
288+
def get_boolean_flag_token(self, token_value: dict, code: str, default_value: Any = None) -> bool:
289+
return self.get_flag_token(token_value, code, default_value, "b")["value"]
211290

212291
def get_string_flag(self, code: str, default_value: Any = None) -> str:
213292
return self.get_flag(code, default_value, "s")["value"]
293+
294+
def get_string_flag_token(self, token_value: dict, code: str, default_value: Any = None) -> str:
295+
return self.get_flag_token(token_value, code, default_value, "s")["value"]
214296

215297
def get_integer_flag(self, code: str, default_value: Any = None) -> int:
216298
return self.get_flag(code, default_value, "i")["value"]
299+
300+
def get_integer_flag_token(self, token_value: dict, code: str, default_value: Any = None) -> int:
301+
return self.get_flag_token(token_value, code, default_value, "i")["value"]
217302

218303
def call_api(self, *args, **kwargs) -> Any:
219304
self._get_or_refresh_access_token()
@@ -248,6 +333,34 @@ def _decode_token_if_needed(self, token_name: str) -> None:
248333
self.__decoded_tokens[token_name] = jwt.decode(**decode_token_params)
249334
else:
250335
raise KindeTokenException(f"Token {token_name} doesn't exist.")
336+
337+
def _decode_token_if_needed_value(self, token_name: str, token_value: dict) -> dict:
338+
if token_name not in token_value:
339+
if not token_value:
340+
raise KindeTokenException(
341+
"Access token doesn't exist.\n"
342+
"When grant_type is CLIENT_CREDENTIALS use fetch_token().\n"
343+
'For other grant_type use "get_login_url()" or "get_register_url()".'
344+
)
345+
token = token_value.get(token_name)
346+
347+
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
348+
349+
if token:
350+
decode_token_params = {
351+
"jwt":token,
352+
"key": signing_key.key,
353+
"algorithms":["RS256"],
354+
"options":{
355+
"verify_signature": True,
356+
"verify_exp": True,
357+
"verify_aud": False
358+
}
359+
}
360+
return jwt.decode(**decode_token_params)
361+
else:
362+
raise KindeTokenException(f"Token {token_name} doesn't exist.")
363+
return token_value
251364

252365
def fetch_token(self, authorization_response: Optional[str] = None) -> None:
253366
if self.grant_type == GrantType.CLIENT_CREDENTIALS:
@@ -274,6 +387,31 @@ def fetch_token(self, authorization_response: Optional[str] = None) -> None:
274387
self.configuration.access_token = self.__access_token_obj.get("access_token")
275388
self._clear_decoded_tokens()
276389

390+
def fetch_token_value(self, authorization_response: Optional[str] = None) -> dict:
391+
if self.grant_type == GrantType.CLIENT_CREDENTIALS:
392+
params = {"grant_type": "client_credentials"}
393+
if self.audience:
394+
params["audience"] = self.audience
395+
else:
396+
if authorization_response is None:
397+
raise KindeConfigurationException(
398+
'"authorization_response" parameter is required when grant_type is different than CLIENT_CREDENTIALS.'
399+
)
400+
params = {"authorization_response": authorization_response}
401+
if self.grant_type == GrantType.AUTHORIZATION_CODE_WITH_PKCE:
402+
params["code_verifier"] = self.code_verifier
403+
404+
access_token_obj = self.client.fetch_token(
405+
self.token_endpoint,
406+
headers={
407+
"Content-Type": "application/x-www-form-urlencoded",
408+
"Kinde-SDK": "/".join(("Python", kinde_sdk_version)),
409+
},
410+
**params,
411+
)
412+
return access_token_obj
413+
414+
277415
def _get_or_refresh_access_token(self) -> None:
278416
if self.grant_type == GrantType.CLIENT_CREDENTIALS:
279417
if not self.__access_token_obj or self.__access_token_obj.is_expired():
@@ -309,6 +447,27 @@ def _refresh_token(self) -> None:
309447
self._clear_decoded_tokens()
310448
else:
311449
raise KindeTokenException('"Access token" and "Refresh token" are invalid.')
450+
451+
def _refresh_token_value(self, token_value: dict) -> dict:
452+
refresh_token = token_value.get("refresh_token")
453+
454+
if refresh_token:
455+
token_value = self.client.refresh_token(
456+
self.token_endpoint,
457+
headers={
458+
"Content-Type": "application/x-www-form-urlencoded",
459+
"Kinde-SDK": "/".join(("Python", kinde_sdk_version)),
460+
},
461+
refresh_token=refresh_token,
462+
)
463+
if not token_value:
464+
raise KindeTokenException(
465+
'"Access token" and "Refresh token" are invalid.'
466+
)
467+
468+
return token_value
469+
else:
470+
raise KindeTokenException('"Access token" and "Refresh token" are invalid.')
312471

313472
def _add_additional_params(self, url: str, additional_params: Optional[Dict[str, str]] = None) -> str:
314473

kinde_sdk/test/test_kinde_api_client.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ def test_fetch_token_authorization_code(self):
9898
client.fetch_token(authorization_response="https://example.com/callback?code=test_code")
9999
self.mock_oauth2_session.return_value.fetch_token.assert_called_once()
100100

101+
def test_fetch_token_authorization_code_token(self):
102+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
103+
self.mock_oauth2_session.return_value.fetch_token.return_value = {"access_token": "test_token"}
104+
token_value = client.fetch_token_value(authorization_response="https://example.com/callback?code=test_code")
105+
self.mock_oauth2_session.return_value.fetch_token.assert_called_once()
106+
101107
@patch('kinde_sdk.kinde_api_client.ApiClient.call_api')
102108
def test_super_call_api_with_correct_args(self, mock_super_call_api):
103109
client = self._create_kinde_client(GrantType.CLIENT_CREDENTIALS)
@@ -142,6 +148,86 @@ def mock_get_claim_side_effect(key):
142148
mock_get_claim.assert_any_call("permissions")
143149
self.assertEqual(mock_get_claim.call_count, 2)
144150

151+
def test_get_permissions_token(self):
152+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
153+
154+
result = client.get_permissions_token({"access_token":{"org_code":"org123","permissions": ["read", "write", "delete"]}})
155+
156+
expected_result = {
157+
"org_code": "org123",
158+
"permissions": ["read", "write", "delete"]
159+
}
160+
self.assertEqual(result, expected_result)
161+
162+
def test_get_permission_token(self):
163+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
164+
165+
result = client.get_permission_token({"access_token":{"org_code":"org123","permissions": ["read", "write", "delete"]}},"read")
166+
167+
expected_result = {
168+
"org_code": "org123",
169+
"is_granted": True
170+
}
171+
self.assertEqual(result, expected_result)
172+
173+
174+
def test_get_claim_token(self):
175+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
176+
177+
result = client.get_claim_token({"access_token":{"org_code":"org123","permissions": ["read", "write", "delete"]}},"org_code")
178+
179+
expected_result = {
180+
"name": "org_code",
181+
"value": "org123"
182+
}
183+
self.assertEqual(result, expected_result)
184+
185+
def test_get_claim_token(self):
186+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
187+
188+
result = client.get_user_details_token({
189+
"access_token":{"org_code":"org123","permissions": ["read", "write", "delete"]}
190+
,"id_token":{"sub":"123","given_name":"John","family_name":"Doe","email":"john@example.com","picture":"https://example.com/pic.jpg"}
191+
})
192+
193+
expected_result = {
194+
"id":"123","given_name":"John","family_name":"Doe","email":"john@example.com","picture":"https://example.com/pic.jpg"
195+
}
196+
self.assertEqual(result, expected_result)
197+
198+
199+
def test_get_flag_token(self):
200+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
201+
202+
result = client.get_flag_token({
203+
"access_token":{"org_code":"org123","permissions": ["read", "write", "delete"],"feature_flags":{"test_flag":{"v":True,"t":"b"}}}
204+
,"id_token":{"sub":"123","given_name":"John","family_name":"Doe","email":"john@example.com","picture":"https://example.com/pic.jpg"}
205+
},
206+
"test_flag"
207+
)
208+
209+
expected_result = {
210+
"code":"test_flag",
211+
"value":True,
212+
"is_default":False,
213+
"type":"boolean"
214+
}
215+
self.assertEqual(result, expected_result)
216+
217+
def test_get_boolean_flag_token(self):
218+
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
219+
220+
result = client.get_boolean_flag_token({
221+
"access_token":{"org_code":"org123","permissions": ["read", "write", "delete"],"feature_flags":{"test_flag":{"v":True,"t":"b"}}}
222+
,"id_token":{"sub":"123","given_name":"John","family_name":"Doe","email":"john@example.com","picture":"https://example.com/pic.jpg"}
223+
},
224+
"test_flag"
225+
)
226+
227+
expected_result = True
228+
self.assertEqual(result, expected_result)
229+
230+
145231
def test_fetch_token_headers_with_authorization_code(self):
146232
client = self._create_kinde_client(GrantType.AUTHORIZATION_CODE)
147233
self.mock_oauth2_session.return_value.fetch_token.return_value = {"access_token": "test_token"}

0 commit comments

Comments
 (0)