usso 0.26.0__tar.gz → 0.27.0__tar.gz

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.
Files changed (27) hide show
  1. {usso-0.26.0/src/usso.egg-info → usso-0.27.0}/PKG-INFO +1 -1
  2. {usso-0.26.0 → usso-0.27.0}/pyproject.toml +1 -1
  3. {usso-0.26.0 → usso-0.27.0}/src/usso/core.py +45 -4
  4. {usso-0.26.0 → usso-0.27.0}/src/usso/fastapi/integration.py +16 -2
  5. usso-0.27.0/src/usso/httpx_session.py +87 -0
  6. {usso-0.26.0 → usso-0.27.0/src/usso.egg-info}/PKG-INFO +1 -1
  7. {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/SOURCES.txt +1 -0
  8. {usso-0.26.0 → usso-0.27.0}/LICENSE.txt +0 -0
  9. {usso-0.26.0 → usso-0.27.0}/README.md +0 -0
  10. {usso-0.26.0 → usso-0.27.0}/setup.cfg +0 -0
  11. {usso-0.26.0 → usso-0.27.0}/src/usso/__init__.py +0 -0
  12. {usso-0.26.0 → usso-0.27.0}/src/usso/api.py +0 -0
  13. {usso-0.26.0 → usso-0.27.0}/src/usso/async_api.py +0 -0
  14. {usso-0.26.0 → usso-0.27.0}/src/usso/async_session.py +0 -0
  15. {usso-0.26.0 → usso-0.27.0}/src/usso/b64tools.py +0 -0
  16. {usso-0.26.0 → usso-0.27.0}/src/usso/django/__init__.py +0 -0
  17. {usso-0.26.0 → usso-0.27.0}/src/usso/django/middleware.py +0 -0
  18. {usso-0.26.0 → usso-0.27.0}/src/usso/exceptions.py +0 -0
  19. {usso-0.26.0 → usso-0.27.0}/src/usso/fastapi/__init__.py +0 -0
  20. {usso-0.26.0 → usso-0.27.0}/src/usso/session.py +0 -0
  21. {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/dependency_links.txt +0 -0
  22. {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/entry_points.txt +0 -0
  23. {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/requires.txt +0 -0
  24. {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/top_level.txt +0 -0
  25. {usso-0.26.0 → usso-0.27.0}/tests/test_api.py +0 -0
  26. {usso-0.26.0 → usso-0.27.0}/tests/test_core.py +0 -0
  27. {usso-0.26.0 → usso-0.27.0}/tests/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices.
5
5
  Author-email: Mahdi Kiani <mahdikiany@gmail.com>
6
6
  Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "usso"
7
- version = "0.26.0"
7
+ version = "0.27.0"
8
8
  description = "A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -2,10 +2,12 @@ import json
2
2
  import logging
3
3
  import os
4
4
  import uuid
5
- from functools import lru_cache
5
+ from urllib.parse import urlparse
6
6
 
7
7
  import cachetools.func
8
8
  import jwt
9
+ import requests
10
+ from cachetools import TTLCache, cached
9
11
  from pydantic import BaseModel, model_validator
10
12
 
11
13
  from . import b64tools
@@ -90,7 +92,7 @@ def decode_token(key, token: str, algorithms=["RS256"], **kwargs) -> dict:
90
92
  logger.error(e)
91
93
 
92
94
 
93
- @lru_cache
95
+ @cached(TTLCache(maxsize=128, ttl=10 * 60))
94
96
  def get_jwk_keys(jwk_url: str) -> jwt.PyJWKClient:
95
97
  return jwt.PyJWKClient(jwk_url, headers={"User-Agent": "usso-python"})
96
98
 
@@ -115,6 +117,15 @@ def decode_token_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
115
117
  logger.error(e)
116
118
 
117
119
 
120
+ @cached(TTLCache(maxsize=128, ttl=10 * 60))
121
+ def get_api_key_data(jwk_url: str, api_key: str):
122
+ parsed = urlparse(jwk_url)
123
+ url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
124
+ response = requests.post(url, json={"api_key": api_key})
125
+ response.raise_for_status()
126
+ return UserData(**response.json())
127
+
128
+
118
129
  class JWTConfig(BaseModel):
119
130
  jwk_url: str | None = None
120
131
  secret: str | None = None
@@ -147,7 +158,9 @@ class Usso:
147
158
  def __init__(
148
159
  self,
149
160
  *,
150
- jwt_config: str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None = None,
161
+ jwt_config: (
162
+ str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
163
+ ) = None,
151
164
  jwk_url: str | None = None,
152
165
  secret: str | None = None,
153
166
  ):
@@ -160,7 +173,7 @@ class Usso:
160
173
  if jwk_url:
161
174
  self.jwt_configs = [JWTConfig(jwk_url=jwk_url)]
162
175
  return
163
-
176
+
164
177
  if not secret:
165
178
  secret = os.getenv("USSO_SECRET")
166
179
  if secret:
@@ -216,3 +229,31 @@ class Usso:
216
229
  status_code=401,
217
230
  error="unauthorized",
218
231
  )
232
+
233
+ def user_data_api_key(self, api_key: str, **kwargs) -> UserData | None:
234
+ """get user data from auth server by api_key."""
235
+ for jwk_config in self.jwt_configs:
236
+ try:
237
+ user_data = jwk_config.decode(api_key)
238
+ if user_data.token_type.lower() != kwargs.get("token_type", "access"):
239
+ raise USSOException(
240
+ status_code=401,
241
+ error="invalid_token_type",
242
+ message="Token type must be 'access'",
243
+ )
244
+
245
+ return user_data
246
+
247
+ except USSOException as e:
248
+ exp = e
249
+
250
+ if kwargs.get("raise_exception", True):
251
+ if exp:
252
+ raise exp
253
+ raise USSOException(
254
+ status_code=401,
255
+ error="unauthorized",
256
+ )
257
+
258
+ def user_data_from_api_key(self, api_key: str):
259
+ return get_api_key_data(self.jwt_configs[0].jwk_url, api_key)
@@ -27,16 +27,26 @@ def get_request_token(request: Request | WebSocket) -> UserData | None:
27
27
  return token
28
28
 
29
29
 
30
- def jwt_access_security_None(request: Request, jwt_config = None) -> UserData | None:
30
+ def jwt_access_security_None(request: Request, jwt_config=None) -> UserData | None:
31
31
  """Return the user associated with a token value."""
32
+ api_key = request.headers.get("x-api-key")
33
+ if api_key:
34
+ return Usso(jwt_config=jwt_config).user_data_from_api_key(api_key)
35
+
32
36
  token = get_request_token(request)
33
37
  if not token:
34
38
  return None
35
- return Usso(jwt_config=jwt_config).user_data_from_token(token, raise_exception=False)
39
+ return Usso(jwt_config=jwt_config).user_data_from_token(
40
+ token, raise_exception=False
41
+ )
36
42
 
37
43
 
38
44
  def jwt_access_security(request: Request, jwt_config=None) -> UserData | None:
39
45
  """Return the user associated with a token value."""
46
+ api_key = request.headers.get("x-api-key")
47
+ if api_key:
48
+ return Usso(jwt_config=jwt_config).user_data_from_api_key(api_key)
49
+
40
50
  token = get_request_token(request)
41
51
  if not token:
42
52
  raise USSOException(
@@ -50,6 +60,10 @@ def jwt_access_security(request: Request, jwt_config=None) -> UserData | None:
50
60
 
51
61
  def jwt_access_security_ws(websocket: WebSocket, jwt_config=None) -> UserData | None:
52
62
  """Return the user associated with a token value."""
63
+ api_key = websocket.headers.get("x-api-key")
64
+ if api_key:
65
+ return Usso(jwt_config=jwt_config).user_data_from_api_key(api_key)
66
+
53
67
  token = get_request_token(websocket)
54
68
  if not token:
55
69
  raise USSOException(
@@ -0,0 +1,87 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import httpx
4
+ import jwt
5
+
6
+
7
+ class AsyncUssoSession(httpx.AsyncClient):
8
+ def __init__(
9
+ self,
10
+ sso_refresh_url: str,
11
+ refresh_token: str | None = None,
12
+ api_key: str | None = None,
13
+ user_id: str | None = None,
14
+ ):
15
+ super().__init__()
16
+ self.sso_refresh_url = sso_refresh_url
17
+ self._refresh_token = refresh_token
18
+ self.access_token = None
19
+ self.session = None # This will hold the aiohttp session
20
+ self.api_key = api_key
21
+ self.user_id = user_id
22
+
23
+ @property
24
+ def refresh_token(self):
25
+ if self._refresh_token:
26
+ decoded_token = jwt.decode(
27
+ self._refresh_token, options={"verify_signature": False}
28
+ )
29
+ exp = decoded_token.get(
30
+ "exp", (datetime.now() + timedelta(days=1)).timestamp()
31
+ )
32
+ exp = datetime.fromtimestamp(exp)
33
+ if exp < datetime.now():
34
+ self._refresh_token = None
35
+
36
+ return self._refresh_token
37
+
38
+ async def _refresh_api(self):
39
+ params = {"user_id": self.user_id} if self.user_id else {}
40
+ async with httpx.AsyncClient() as session:
41
+ response = await session.get(
42
+ f"{self.sso_refresh_url}/api",
43
+ headers={"x-api-key": self.api_key},
44
+ params=params,
45
+ )
46
+ response.raise_for_status()
47
+ data: dict = response.json()
48
+ self._refresh_token = data.get("token", {}).get("refresh_token")
49
+
50
+ async def _refresh(self):
51
+ if not self.refresh_token and not self.api_key:
52
+ raise ValueError("Refresh token not provided or invalid.")
53
+
54
+ if self.api_key and not self.refresh_token:
55
+ await self._refresh_api()
56
+
57
+ async with httpx.AsyncClient() as session:
58
+ response = await session.post(
59
+ self.sso_refresh_url, json={"refresh_token": self.refresh_token}
60
+ )
61
+ response.raise_for_status()
62
+ return response.json()
63
+
64
+ async def _ensure_valid_token(self):
65
+ if self.access_token:
66
+ decoded_token = jwt.decode(
67
+ self.access_token, options={"verify_signature": False}
68
+ )
69
+ exp = decoded_token.get("exp")
70
+
71
+ if exp and datetime.fromtimestamp(exp) < datetime.now():
72
+ self.access_token = None # Token expired, need a new one
73
+
74
+ if not self.access_token:
75
+ # Get a new token if none exists or it has expired
76
+ token_data = await self._refresh()
77
+ self.access_token = token_data.get("access_token")
78
+
79
+ async def request(self, method: str, url: str, *args, **kwargs):
80
+ await self._ensure_valid_token()
81
+
82
+ # Add authorization header to each request
83
+ headers = kwargs.pop("headers") or {}
84
+ headers["Authorization"] = f"Bearer {self.access_token}"
85
+
86
+ # Call the parent's request method
87
+ return await super().request(method, url, headers=headers, *args, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices.
5
5
  Author-email: Mahdi Kiani <mahdikiany@gmail.com>
6
6
  Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
@@ -8,6 +8,7 @@ src/usso/async_session.py
8
8
  src/usso/b64tools.py
9
9
  src/usso/core.py
10
10
  src/usso/exceptions.py
11
+ src/usso/httpx_session.py
11
12
  src/usso/session.py
12
13
  src/usso.egg-info/PKG-INFO
13
14
  src/usso.egg-info/SOURCES.txt
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes