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.
- {usso-0.26.0/src/usso.egg-info → usso-0.27.0}/PKG-INFO +1 -1
- {usso-0.26.0 → usso-0.27.0}/pyproject.toml +1 -1
- {usso-0.26.0 → usso-0.27.0}/src/usso/core.py +45 -4
- {usso-0.26.0 → usso-0.27.0}/src/usso/fastapi/integration.py +16 -2
- usso-0.27.0/src/usso/httpx_session.py +87 -0
- {usso-0.26.0 → usso-0.27.0/src/usso.egg-info}/PKG-INFO +1 -1
- {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/SOURCES.txt +1 -0
- {usso-0.26.0 → usso-0.27.0}/LICENSE.txt +0 -0
- {usso-0.26.0 → usso-0.27.0}/README.md +0 -0
- {usso-0.26.0 → usso-0.27.0}/setup.cfg +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/__init__.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/api.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/async_api.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/async_session.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/b64tools.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/django/__init__.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/django/middleware.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/exceptions.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/fastapi/__init__.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso/session.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/dependency_links.txt +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/entry_points.txt +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/requires.txt +0 -0
- {usso-0.26.0 → usso-0.27.0}/src/usso.egg-info/top_level.txt +0 -0
- {usso-0.26.0 → usso-0.27.0}/tests/test_api.py +0 -0
- {usso-0.26.0 → usso-0.27.0}/tests/test_core.py +0 -0
- {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.
|
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.
|
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
|
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
|
-
@
|
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:
|
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
|
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(
|
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.
|
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>
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|