usso 0.26.1__py3-none-any.whl → 0.27.1__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.
- usso/client/__init__.py +0 -0
- usso/core.py +55 -40
- usso/django/middleware.py +0 -1
- usso/fastapi/integration.py +17 -3
- usso/schemas.py +39 -0
- usso/session/__init__.py +0 -0
- usso/session/async_session.py +65 -0
- usso/session/session.py +118 -0
- {usso-0.26.1.dist-info → usso-0.27.1.dist-info}/METADATA +18 -18
- usso-0.27.1.dist-info/RECORD +21 -0
- {usso-0.26.1.dist-info → usso-0.27.1.dist-info}/WHEEL +1 -1
- usso/async_session.py +0 -124
- usso/httpx_session.py +0 -87
- usso/session.py +0 -78
- usso-0.26.1.dist-info/RECORD +0 -19
- /usso/{api.py → client/api.py} +0 -0
- /usso/{async_api.py → client/async_api.py} +0 -0
- {usso-0.26.1.dist-info → usso-0.27.1.dist-info}/LICENSE.txt +0 -0
- {usso-0.26.1.dist-info → usso-0.27.1.dist-info}/entry_points.txt +0 -0
- {usso-0.26.1.dist-info → usso-0.27.1.dist-info}/top_level.txt +0 -0
usso/client/__init__.py
ADDED
File without changes
|
usso/core.py
CHANGED
@@ -1,53 +1,21 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
3
|
import os
|
4
|
-
import
|
5
|
-
from
|
4
|
+
from datetime import datetime, timedelta
|
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
|
-
from . import b64tools
|
12
13
|
from .exceptions import USSOException
|
14
|
+
from .schemas import UserData
|
13
15
|
|
14
16
|
logger = logging.getLogger("usso")
|
15
17
|
|
16
18
|
|
17
|
-
class UserData(BaseModel):
|
18
|
-
user_id: str
|
19
|
-
workspace_id: str | None = None
|
20
|
-
workspace_ids: list[str] = []
|
21
|
-
token_type: str = "access"
|
22
|
-
|
23
|
-
email: str | None = None
|
24
|
-
phone: str | None = None
|
25
|
-
username: str | None = None
|
26
|
-
|
27
|
-
authentication_method: str | None = None
|
28
|
-
is_active: bool = False
|
29
|
-
|
30
|
-
jti: str | None = None
|
31
|
-
data: dict | None = None
|
32
|
-
|
33
|
-
token: str | None = None
|
34
|
-
|
35
|
-
@property
|
36
|
-
def uid(self) -> uuid.UUID:
|
37
|
-
user_id = self.user_id
|
38
|
-
|
39
|
-
if user_id.startswith("u_"):
|
40
|
-
user_id = user_id[2:]
|
41
|
-
if 22 <= len(user_id) <= 24:
|
42
|
-
user_id = b64tools.b64_decode_uuid(user_id)
|
43
|
-
|
44
|
-
return uuid.UUID(user_id)
|
45
|
-
|
46
|
-
@property
|
47
|
-
def b64id(self) -> uuid.UUID:
|
48
|
-
return b64tools.b64_encode_uuid_strip(self.uid)
|
49
|
-
|
50
|
-
|
51
19
|
def get_authorization_scheme_param(
|
52
20
|
authorization_header_value: str | None,
|
53
21
|
) -> tuple[str, str]:
|
@@ -90,7 +58,15 @@ def decode_token(key, token: str, algorithms=["RS256"], **kwargs) -> dict:
|
|
90
58
|
logger.error(e)
|
91
59
|
|
92
60
|
|
93
|
-
|
61
|
+
def is_expired(token: str, **kwargs) -> bool:
|
62
|
+
now = datetime.now()
|
63
|
+
decoded_token: dict = jwt.decode(token, options={"verify_signature": False})
|
64
|
+
exp = decoded_token.get("exp", (now + timedelta(days=1)).timestamp())
|
65
|
+
exp = datetime.fromtimestamp(exp)
|
66
|
+
return exp >= now
|
67
|
+
|
68
|
+
|
69
|
+
@cached(TTLCache(maxsize=128, ttl=10 * 60))
|
94
70
|
def get_jwk_keys(jwk_url: str) -> jwt.PyJWKClient:
|
95
71
|
return jwt.PyJWKClient(jwk_url, headers={"User-Agent": "usso-python"})
|
96
72
|
|
@@ -115,6 +91,15 @@ def decode_token_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
|
|
115
91
|
logger.error(e)
|
116
92
|
|
117
93
|
|
94
|
+
@cached(TTLCache(maxsize=128, ttl=10 * 60))
|
95
|
+
def get_api_key_data(jwk_url: str, api_key: str):
|
96
|
+
parsed = urlparse(jwk_url)
|
97
|
+
url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
|
98
|
+
response = requests.post(url, json={"api_key": api_key})
|
99
|
+
response.raise_for_status()
|
100
|
+
return UserData(**response.json())
|
101
|
+
|
102
|
+
|
118
103
|
class JWTConfig(BaseModel):
|
119
104
|
jwk_url: str | None = None
|
120
105
|
secret: str | None = None
|
@@ -147,7 +132,9 @@ class Usso:
|
|
147
132
|
def __init__(
|
148
133
|
self,
|
149
134
|
*,
|
150
|
-
jwt_config:
|
135
|
+
jwt_config: (
|
136
|
+
str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
|
137
|
+
) = None,
|
151
138
|
jwk_url: str | None = None,
|
152
139
|
secret: str | None = None,
|
153
140
|
):
|
@@ -160,7 +147,7 @@ class Usso:
|
|
160
147
|
if jwk_url:
|
161
148
|
self.jwt_configs = [JWTConfig(jwk_url=jwk_url)]
|
162
149
|
return
|
163
|
-
|
150
|
+
|
164
151
|
if not secret:
|
165
152
|
secret = os.getenv("USSO_SECRET")
|
166
153
|
if secret:
|
@@ -216,3 +203,31 @@ class Usso:
|
|
216
203
|
status_code=401,
|
217
204
|
error="unauthorized",
|
218
205
|
)
|
206
|
+
|
207
|
+
def user_data_api_key(self, api_key: str, **kwargs) -> UserData | None:
|
208
|
+
"""get user data from auth server by api_key."""
|
209
|
+
for jwk_config in self.jwt_configs:
|
210
|
+
try:
|
211
|
+
user_data = jwk_config.decode(api_key)
|
212
|
+
if user_data.token_type.lower() != kwargs.get("token_type", "access"):
|
213
|
+
raise USSOException(
|
214
|
+
status_code=401,
|
215
|
+
error="invalid_token_type",
|
216
|
+
message="Token type must be 'access'",
|
217
|
+
)
|
218
|
+
|
219
|
+
return user_data
|
220
|
+
|
221
|
+
except USSOException as e:
|
222
|
+
exp = e
|
223
|
+
|
224
|
+
if kwargs.get("raise_exception", True):
|
225
|
+
if exp:
|
226
|
+
raise exp
|
227
|
+
raise USSOException(
|
228
|
+
status_code=401,
|
229
|
+
error="unauthorized",
|
230
|
+
)
|
231
|
+
|
232
|
+
def user_data_from_api_key(self, api_key: str):
|
233
|
+
return get_api_key_data(self.jwt_configs[0].jwk_url, api_key)
|
usso/django/middleware.py
CHANGED
@@ -6,7 +6,6 @@ from django.db.utils import IntegrityError
|
|
6
6
|
from django.http import JsonResponse
|
7
7
|
from django.http.request import HttpRequest
|
8
8
|
from django.utils.deprecation import MiddlewareMixin
|
9
|
-
|
10
9
|
from usso import UserData, Usso, USSOException
|
11
10
|
|
12
11
|
logger = logging.getLogger("usso")
|
usso/fastapi/integration.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import logging
|
2
2
|
|
3
|
-
from fastapi import Request, WebSocket
|
4
3
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
5
4
|
|
5
|
+
from fastapi import Request, WebSocket
|
6
6
|
from usso.exceptions import USSOException
|
7
7
|
|
8
8
|
from ..core import UserData, Usso, get_authorization_scheme_param
|
@@ -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(
|
usso/schemas.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
import uuid
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from . import b64tools
|
6
|
+
|
7
|
+
|
8
|
+
class UserData(BaseModel):
|
9
|
+
user_id: str
|
10
|
+
workspace_id: str | None = None
|
11
|
+
workspace_ids: list[str] = []
|
12
|
+
token_type: str = "access"
|
13
|
+
|
14
|
+
email: str | None = None
|
15
|
+
phone: str | None = None
|
16
|
+
username: str | None = None
|
17
|
+
|
18
|
+
authentication_method: str | None = None
|
19
|
+
is_active: bool = False
|
20
|
+
|
21
|
+
jti: str | None = None
|
22
|
+
data: dict | None = None
|
23
|
+
|
24
|
+
token: str | None = None
|
25
|
+
|
26
|
+
@property
|
27
|
+
def uid(self) -> uuid.UUID:
|
28
|
+
user_id = self.user_id
|
29
|
+
|
30
|
+
if user_id.startswith("u_"):
|
31
|
+
user_id = user_id[2:]
|
32
|
+
if 22 <= len(user_id) <= 24:
|
33
|
+
user_id = b64tools.b64_decode_uuid(user_id)
|
34
|
+
|
35
|
+
return uuid.UUID(user_id)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def b64id(self) -> uuid.UUID:
|
39
|
+
return b64tools.b64_encode_uuid_strip(self.uid)
|
usso/session/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import httpx
|
2
|
+
|
3
|
+
from ..core import is_expired
|
4
|
+
from .session import BaseUssoSession
|
5
|
+
|
6
|
+
|
7
|
+
class AsyncUssoSession(httpx.AsyncClient, BaseUssoSession):
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
usso_base_url: str | None = None,
|
11
|
+
api_key: str | None = None,
|
12
|
+
usso_refresh_url: str | None = None,
|
13
|
+
refresh_token: str | None = None,
|
14
|
+
usso_api_key: str | None = None,
|
15
|
+
user_id: str | None = None,
|
16
|
+
):
|
17
|
+
BaseUssoSession.__init__(
|
18
|
+
self,
|
19
|
+
usso_base_url,
|
20
|
+
api_key,
|
21
|
+
usso_refresh_url,
|
22
|
+
refresh_token,
|
23
|
+
usso_api_key,
|
24
|
+
user_id,
|
25
|
+
)
|
26
|
+
httpx.AsyncClient.__init__(self, headers=self.headers)
|
27
|
+
|
28
|
+
async def _refresh_api(self):
|
29
|
+
params = {"user_id": self.user_id} if self.user_id else {}
|
30
|
+
response = await self.get(
|
31
|
+
f"{self.usso_refresh_url}/api",
|
32
|
+
headers={"x-api-key": self.usso_api_key},
|
33
|
+
params=params,
|
34
|
+
)
|
35
|
+
response.raise_for_status()
|
36
|
+
data: dict = response.json()
|
37
|
+
self._refresh_token = data.get("token", {}).get("refresh_token")
|
38
|
+
|
39
|
+
async def _refresh(self):
|
40
|
+
assert (
|
41
|
+
self.refresh_token or self.usso_api_key
|
42
|
+
), "refresh_token or usso_api_key is required"
|
43
|
+
|
44
|
+
if self.usso_api_key and not self.refresh_token:
|
45
|
+
await self._refresh_api()
|
46
|
+
|
47
|
+
response = await self.post(
|
48
|
+
self.usso_refresh_url, json={"refresh_token": f"{self.refresh_token}"}
|
49
|
+
)
|
50
|
+
response.raise_for_status()
|
51
|
+
self.access_token = response.json().get("access_token")
|
52
|
+
self.headers.update({"Authorization": f"Bearer {self.access_token}"})
|
53
|
+
return response.json()
|
54
|
+
|
55
|
+
async def get_session(self):
|
56
|
+
if self.api_key:
|
57
|
+
return self
|
58
|
+
|
59
|
+
if not self.access_token or is_expired(self.access_token):
|
60
|
+
await self._refresh()
|
61
|
+
return self
|
62
|
+
|
63
|
+
async def _request(self, method: str, url: str, **kwargs):
|
64
|
+
session = await self.get_session()
|
65
|
+
return await session.request(method, url, **kwargs)
|
usso/session/session.py
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
from urllib.parse import urlparse
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from singleton import Singleton
|
5
|
+
|
6
|
+
from ..core import is_expired
|
7
|
+
|
8
|
+
|
9
|
+
class BaseUssoSession(metaclass=Singleton):
|
10
|
+
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
usso_base_url: str | None = None,
|
14
|
+
api_key: str | None = None,
|
15
|
+
usso_refresh_url: str | None = None,
|
16
|
+
refresh_token: str | None = None,
|
17
|
+
usso_api_key: str | None = None,
|
18
|
+
user_id: str | None = None,
|
19
|
+
):
|
20
|
+
assert (
|
21
|
+
usso_base_url or usso_refresh_url
|
22
|
+
), "usso_base_url or usso_refresh_url is required"
|
23
|
+
assert (
|
24
|
+
refresh_token or api_key or usso_api_key
|
25
|
+
), "refresh_token or api_key or usso_api_key is required"
|
26
|
+
|
27
|
+
if not usso_base_url:
|
28
|
+
url_parts = urlparse(usso_refresh_url)
|
29
|
+
usso_base_url = f"{url_parts.scheme}://{url_parts.netloc}"
|
30
|
+
if usso_base_url.endswith("/"):
|
31
|
+
usso_base_url = usso_base_url[:-1]
|
32
|
+
|
33
|
+
self.usso_refresh_url = usso_refresh_url or f"{usso_base_url}/auth/refresh"
|
34
|
+
self._refresh_token = refresh_token
|
35
|
+
self.session = requests.Session()
|
36
|
+
self.access_token = None
|
37
|
+
self.api_key = api_key
|
38
|
+
self.usso_api_key = usso_api_key
|
39
|
+
self.user_id = user_id
|
40
|
+
self.headers = {}
|
41
|
+
if api_key:
|
42
|
+
self.headers = {"x-api-key": api_key}
|
43
|
+
self.session.headers.update(self.headers)
|
44
|
+
|
45
|
+
@property
|
46
|
+
def refresh_token(self):
|
47
|
+
if self._refresh_token and is_expired(self._refresh_token):
|
48
|
+
self._refresh_token = None
|
49
|
+
|
50
|
+
return self._refresh_token
|
51
|
+
|
52
|
+
def request(self, method: str, url: str, **kwargs):
|
53
|
+
return self._request(method, url, **kwargs)
|
54
|
+
|
55
|
+
def get(self, url: str, **kwargs):
|
56
|
+
return self._request("GET", url, **kwargs)
|
57
|
+
|
58
|
+
def post(self, url: str, **kwargs):
|
59
|
+
return self._request("POST", url, **kwargs)
|
60
|
+
|
61
|
+
def put(self, url: str, **kwargs):
|
62
|
+
return self._request("PUT", url, **kwargs)
|
63
|
+
|
64
|
+
def patch(self, url: str, **kwargs):
|
65
|
+
return self._request("PATCH", url, **kwargs)
|
66
|
+
|
67
|
+
def delete(self, url: str, **kwargs):
|
68
|
+
return self._request("DELETE", url, **kwargs)
|
69
|
+
|
70
|
+
def head(self, url: str, **kwargs):
|
71
|
+
return self._request("HEAD", url, **kwargs)
|
72
|
+
|
73
|
+
def options(self, url: str, **kwargs):
|
74
|
+
return self._request("OPTIONS", url, **kwargs)
|
75
|
+
|
76
|
+
|
77
|
+
class UssoSession(BaseUssoSession):
|
78
|
+
def _refresh_api(self):
|
79
|
+
params = {"user_id": self.user_id} if self.user_id else {}
|
80
|
+
response = requests.get(
|
81
|
+
f"{self.usso_refresh_url}/api",
|
82
|
+
headers={"x-api-key": self.usso_api_key},
|
83
|
+
params=params,
|
84
|
+
)
|
85
|
+
response.raise_for_status()
|
86
|
+
data: dict = response.json()
|
87
|
+
self._refresh_token = data.get("token", {}).get("refresh_token")
|
88
|
+
|
89
|
+
def _refresh(self):
|
90
|
+
assert (
|
91
|
+
self.refresh_token or self.usso_api_key
|
92
|
+
), "refresh_token or usso_api_key is required"
|
93
|
+
|
94
|
+
if self.usso_api_key and not self.refresh_token:
|
95
|
+
self._refresh_api()
|
96
|
+
|
97
|
+
response = requests.post(
|
98
|
+
self.usso_refresh_url, json={"refresh_token": f"{self.refresh_token}"}
|
99
|
+
)
|
100
|
+
response.raise_for_status()
|
101
|
+
self.access_token = response.json().get("access_token")
|
102
|
+
self.session.headers.update({"Authorization": f"Bearer {self.access_token}"})
|
103
|
+
return response.json()
|
104
|
+
|
105
|
+
def get_session(self):
|
106
|
+
if self.api_key:
|
107
|
+
return self.session
|
108
|
+
|
109
|
+
if not self.access_token or is_expired(self.access_token):
|
110
|
+
self._refresh()
|
111
|
+
return self.session
|
112
|
+
|
113
|
+
def _request(self, method: str, url: str, **kwargs):
|
114
|
+
session = self.get_session()
|
115
|
+
return session.request(method, url, **kwargs)
|
116
|
+
|
117
|
+
def close(self):
|
118
|
+
self.session.close()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: usso
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.27.1
|
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>
|
@@ -42,28 +42,28 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
42
42
|
Requires-Python: >=3.9
|
43
43
|
Description-Content-Type: text/markdown
|
44
44
|
License-File: LICENSE.txt
|
45
|
-
Requires-Dist: pydantic
|
46
|
-
Requires-Dist: requests
|
45
|
+
Requires-Dist: pydantic>=2
|
46
|
+
Requires-Dist: requests>=2.26.0
|
47
47
|
Requires-Dist: pyjwt[crypto]
|
48
48
|
Requires-Dist: cachetools
|
49
|
-
Provides-Extra: all
|
50
|
-
Requires-Dist: fastapi ; extra == 'all'
|
51
|
-
Requires-Dist: uvicorn ; extra == 'all'
|
52
|
-
Requires-Dist: django ; extra == 'all'
|
53
|
-
Requires-Dist: httpx ; extra == 'all'
|
54
|
-
Requires-Dist: dev ; extra == 'all'
|
55
|
-
Requires-Dist: test ; extra == 'all'
|
56
|
-
Provides-Extra: dev
|
57
|
-
Requires-Dist: check-manifest ; extra == 'dev'
|
58
|
-
Provides-Extra: django
|
59
|
-
Requires-Dist: Django >=3.2 ; extra == 'django'
|
60
49
|
Provides-Extra: fastapi
|
61
|
-
Requires-Dist: fastapi
|
62
|
-
Requires-Dist: uvicorn[standard]
|
50
|
+
Requires-Dist: fastapi>=0.65.0; extra == "fastapi"
|
51
|
+
Requires-Dist: uvicorn[standard]>=0.13.0; extra == "fastapi"
|
52
|
+
Provides-Extra: django
|
53
|
+
Requires-Dist: Django>=3.2; extra == "django"
|
63
54
|
Provides-Extra: httpx
|
64
|
-
Requires-Dist: httpx
|
55
|
+
Requires-Dist: httpx; extra == "httpx"
|
56
|
+
Provides-Extra: dev
|
57
|
+
Requires-Dist: check-manifest; extra == "dev"
|
65
58
|
Provides-Extra: test
|
66
|
-
Requires-Dist: coverage
|
59
|
+
Requires-Dist: coverage; extra == "test"
|
60
|
+
Provides-Extra: all
|
61
|
+
Requires-Dist: fastapi; extra == "all"
|
62
|
+
Requires-Dist: uvicorn; extra == "all"
|
63
|
+
Requires-Dist: django; extra == "all"
|
64
|
+
Requires-Dist: httpx; extra == "all"
|
65
|
+
Requires-Dist: dev; extra == "all"
|
66
|
+
Requires-Dist: test; extra == "all"
|
67
67
|
|
68
68
|
# USSO-Client
|
69
69
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
usso/__init__.py,sha256=NnOS_S1a-JKTOlGe1nw-kCL3m0y82mA2mDraus7BQ2o,120
|
2
|
+
usso/b64tools.py,sha256=HGQ0E59vzjrQo2-4jrcY03ebtTaYwTtCZ7KgJaEmxO0,610
|
3
|
+
usso/core.py,sha256=KI_61zIxZNDB1QeZsYN7wgKYy54Huhdm2T2WDFq69Sw,8043
|
4
|
+
usso/exceptions.py,sha256=hawOAuVbvQtjgRfwp1KFZ4SmV7fh720y5Gom9JVA8W8,504
|
5
|
+
usso/schemas.py,sha256=nYFqBMtnGJw13cSKSIMIZdxKVz3AIbnETDuiENHdl5g,850
|
6
|
+
usso/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
usso/client/api.py,sha256=xlDq2nZNpq3mhAvqIbGEfANHNjJpPquSeULBfS7iMJw,5094
|
8
|
+
usso/client/async_api.py,sha256=rb-Xh5oudmZrPYM_iH_B75b5Z0Fvi1V1uurdcKE51w0,5551
|
9
|
+
usso/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
usso/django/middleware.py,sha256=CJ_poyF9dUTAm-z34s_2DGts-g30fX2PJgzouxVeQY0,3244
|
11
|
+
usso/fastapi/__init__.py,sha256=0EcdOzb4f3yu9nILIdGWnlyUz-0VaVX2az1e3f2BusI,201
|
12
|
+
usso/fastapi/integration.py,sha256=HCLveG2s6pFsuu2zU0JbgerJ4BgaF712ElmEUPHhwyk,2321
|
13
|
+
usso/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
+
usso/session/async_session.py,sha256=SSfB1tDcq5p0eXd2awfTsVYQWm5IRL-1GeMo1zsVFks,2081
|
15
|
+
usso/session/session.py,sha256=VRxt-HXXqXrieDt1oA3vBYMjj30PFGAFVwIPtu86JYI,3792
|
16
|
+
usso-0.27.1.dist-info/LICENSE.txt,sha256=ceC9ZJOV9H6CtQDcYmHOS46NA3dHJ_WD4J9blH513pc,1081
|
17
|
+
usso-0.27.1.dist-info/METADATA,sha256=hQkZJFA_Z36wxxi9FXBe220pEH81RiO00EKNtOsQiGc,4489
|
18
|
+
usso-0.27.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
19
|
+
usso-0.27.1.dist-info/entry_points.txt,sha256=4Zgpm5ELaAWPf0jPGJFz1_X69H7un8ycT3WdGoJ0Vvk,35
|
20
|
+
usso-0.27.1.dist-info/top_level.txt,sha256=g9Jf6h1Oyidh0vPiFni7UHInTJjSvu6cUalpLTIvthg,5
|
21
|
+
usso-0.27.1.dist-info/RECORD,,
|
usso/async_session.py
DELETED
@@ -1,124 +0,0 @@
|
|
1
|
-
from contextlib import asynccontextmanager
|
2
|
-
from datetime import datetime, timedelta
|
3
|
-
|
4
|
-
import aiohttp
|
5
|
-
import jwt
|
6
|
-
|
7
|
-
|
8
|
-
class AsyncUssoSession:
|
9
|
-
def __init__(
|
10
|
-
self,
|
11
|
-
sso_refresh_url: str,
|
12
|
-
refresh_token: str | None = None,
|
13
|
-
api_key: str | None = None,
|
14
|
-
user_id: str | None = None,
|
15
|
-
):
|
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 aiohttp.ClientSession() as session:
|
41
|
-
async with session.get(
|
42
|
-
f"{self.sso_refresh_url}/api",
|
43
|
-
headers={"x-api-key": self.api_key},
|
44
|
-
params=params,
|
45
|
-
) as response:
|
46
|
-
response.raise_for_status()
|
47
|
-
data: dict = await 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 aiohttp.ClientSession() as session:
|
58
|
-
async with session.post(
|
59
|
-
self.sso_refresh_url,
|
60
|
-
json={"refresh_token": self.refresh_token},
|
61
|
-
) as response:
|
62
|
-
response.raise_for_status()
|
63
|
-
return await response.json()
|
64
|
-
|
65
|
-
async def _ensure_valid_token(self):
|
66
|
-
if self.access_token:
|
67
|
-
decoded_token = jwt.decode(
|
68
|
-
self.access_token, options={"verify_signature": False}
|
69
|
-
)
|
70
|
-
exp = decoded_token.get("exp")
|
71
|
-
|
72
|
-
if exp and datetime.fromtimestamp(exp) < datetime.now():
|
73
|
-
self.access_token = None # Token expired, need a new one
|
74
|
-
|
75
|
-
if not self.access_token:
|
76
|
-
# Get a new token if none exists or it has expired
|
77
|
-
token_data = await self._refresh()
|
78
|
-
self.access_token = token_data.get("access_token")
|
79
|
-
|
80
|
-
# Update headers with the new access token
|
81
|
-
if self.session:
|
82
|
-
self.session.headers.update(
|
83
|
-
{"Authorization": f"Bearer {self.access_token}"}
|
84
|
-
)
|
85
|
-
|
86
|
-
async def __aenter__(self):
|
87
|
-
self.session = aiohttp.ClientSession() # Initialize the session
|
88
|
-
await self._ensure_valid_token() # Ensure valid token before usage
|
89
|
-
return self
|
90
|
-
|
91
|
-
async def __aexit__(self, exc_type, exc_value, traceback):
|
92
|
-
if self.session:
|
93
|
-
await self.session.close() # Close the session properly
|
94
|
-
|
95
|
-
@asynccontextmanager
|
96
|
-
async def _request(self, method: str, url: str, **kwargs):
|
97
|
-
await self._ensure_valid_token() # Ensure valid token before any request
|
98
|
-
async with self.session.request(method, url, **kwargs) as response:
|
99
|
-
yield response
|
100
|
-
|
101
|
-
def get(self, url: str, **kwargs):
|
102
|
-
return self._request("GET", url, **kwargs)
|
103
|
-
|
104
|
-
def post(self, url: str, **kwargs):
|
105
|
-
return self._request("POST", url, **kwargs)
|
106
|
-
|
107
|
-
def put(self, url: str, **kwargs):
|
108
|
-
return self._request("PUT", url, **kwargs)
|
109
|
-
|
110
|
-
def patch(self, url: str, **kwargs):
|
111
|
-
return self._request("PATCH", url, **kwargs)
|
112
|
-
|
113
|
-
def delete(self, url: str, **kwargs):
|
114
|
-
return self._request("DELETE", url, **kwargs)
|
115
|
-
|
116
|
-
def head(self, url: str, **kwargs):
|
117
|
-
return self._request("HEAD", url, **kwargs)
|
118
|
-
|
119
|
-
def options(self, url: str, **kwargs):
|
120
|
-
return self._request("OPTIONS", url, **kwargs)
|
121
|
-
|
122
|
-
async def close(self):
|
123
|
-
await self.session.close()
|
124
|
-
self.session = None
|
usso/httpx_session.py
DELETED
@@ -1,87 +0,0 @@
|
|
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)
|
usso/session.py
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
from datetime import datetime, timedelta
|
2
|
-
|
3
|
-
import jwt
|
4
|
-
import requests
|
5
|
-
|
6
|
-
|
7
|
-
class UssoSession:
|
8
|
-
|
9
|
-
def __init__(
|
10
|
-
self,
|
11
|
-
sso_refresh_url: str,
|
12
|
-
refresh_token: str | None = None,
|
13
|
-
api_key: str | None = None,
|
14
|
-
user_id: str | None = None,
|
15
|
-
):
|
16
|
-
self.sso_refresh_url = sso_refresh_url
|
17
|
-
self._refresh_token = refresh_token
|
18
|
-
self.session = requests.Session()
|
19
|
-
self.access_token = None
|
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
|
-
def _refresh_api(self):
|
39
|
-
params = {"user_id": self.user_id} if self.user_id else {}
|
40
|
-
response = requests.get(
|
41
|
-
f"{self.sso_refresh_url}/api",
|
42
|
-
headers={"x-api-key": self.api_key},
|
43
|
-
params=params,
|
44
|
-
)
|
45
|
-
response.raise_for_status()
|
46
|
-
data = response.json()
|
47
|
-
self._refresh_token = data.get("token", {}).get("refresh_token")
|
48
|
-
|
49
|
-
def _refresh(self):
|
50
|
-
if not self.refresh_token and not self.api_key:
|
51
|
-
return
|
52
|
-
|
53
|
-
if self.api_key and not self.refresh_token:
|
54
|
-
self._refresh_api()
|
55
|
-
|
56
|
-
response = requests.post(
|
57
|
-
self.sso_refresh_url,
|
58
|
-
json={"refresh_token": f"{self.refresh_token}"},
|
59
|
-
)
|
60
|
-
response.raise_for_status()
|
61
|
-
self.access_token = response.json().get("access_token")
|
62
|
-
self.session.headers.update({"Authorization": f"Bearer {self.access_token}"})
|
63
|
-
return response.json()
|
64
|
-
|
65
|
-
def get_session(self):
|
66
|
-
if self.access_token:
|
67
|
-
decoded_token = jwt.decode(
|
68
|
-
self.access_token, options={"verify_signature": False}
|
69
|
-
)
|
70
|
-
exp = datetime.fromtimestamp(decoded_token.get("exp"))
|
71
|
-
if exp < datetime.now():
|
72
|
-
self.access_token = None
|
73
|
-
if not self.access_token:
|
74
|
-
self.access_token = self._refresh().get("access_token")
|
75
|
-
self.session.headers.update(
|
76
|
-
{"Authorization": f"Bearer {self.access_token}"}
|
77
|
-
)
|
78
|
-
return self.session
|
usso-0.26.1.dist-info/RECORD
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
usso/__init__.py,sha256=NnOS_S1a-JKTOlGe1nw-kCL3m0y82mA2mDraus7BQ2o,120
|
2
|
-
usso/api.py,sha256=xlDq2nZNpq3mhAvqIbGEfANHNjJpPquSeULBfS7iMJw,5094
|
3
|
-
usso/async_api.py,sha256=rb-Xh5oudmZrPYM_iH_B75b5Z0Fvi1V1uurdcKE51w0,5551
|
4
|
-
usso/async_session.py,sha256=nFIrtV3Tp0H-s2ZkMLU9_fVSeVGq1EtY1bGT_XOS5Vw,4336
|
5
|
-
usso/b64tools.py,sha256=HGQ0E59vzjrQo2-4jrcY03ebtTaYwTtCZ7KgJaEmxO0,610
|
6
|
-
usso/core.py,sha256=tZzoh_t7HYr-HIual4hN7K1ZVk_nGZdKpaItq5VvkJQ,7087
|
7
|
-
usso/exceptions.py,sha256=hawOAuVbvQtjgRfwp1KFZ4SmV7fh720y5Gom9JVA8W8,504
|
8
|
-
usso/httpx_session.py,sha256=jp52thSbve4gpJuVVxnKEgH5o7LbYIZ5owf3Y-7ZDbY,3067
|
9
|
-
usso/session.py,sha256=E8qx96IWfLWp0CTo1qwb6VUWn0giUnKQIQo-ZRnneEY,2508
|
10
|
-
usso/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
usso/django/middleware.py,sha256=EEEpHvMQ6QiWw2HY8zQ2Aec0RCATcLWsCKeyiPWJKio,3245
|
12
|
-
usso/fastapi/__init__.py,sha256=0EcdOzb4f3yu9nILIdGWnlyUz-0VaVX2az1e3f2BusI,201
|
13
|
-
usso/fastapi/integration.py,sha256=-8MTeqGokvmUO0lxZpEWXdTMYg6n065qtnaJHOwCrzQ,1890
|
14
|
-
usso-0.26.1.dist-info/LICENSE.txt,sha256=ceC9ZJOV9H6CtQDcYmHOS46NA3dHJ_WD4J9blH513pc,1081
|
15
|
-
usso-0.26.1.dist-info/METADATA,sha256=GOWjHISSSaoWSO_Y5iVVrcNovcqxexM7CSpViujFz_0,4506
|
16
|
-
usso-0.26.1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
17
|
-
usso-0.26.1.dist-info/entry_points.txt,sha256=4Zgpm5ELaAWPf0jPGJFz1_X69H7un8ycT3WdGoJ0Vvk,35
|
18
|
-
usso-0.26.1.dist-info/top_level.txt,sha256=g9Jf6h1Oyidh0vPiFni7UHInTJjSvu6cUalpLTIvthg,5
|
19
|
-
usso-0.26.1.dist-info/RECORD,,
|
/usso/{api.py → client/api.py}
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|