usso 0.27.10__py3-none-any.whl → 0.27.12__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/core.py CHANGED
@@ -5,13 +5,11 @@ from datetime import datetime, timedelta
5
5
  from urllib.parse import urlparse
6
6
 
7
7
  import cachetools.func
8
+ import httpx
8
9
  import jwt
9
- import requests
10
- from cachetools import TTLCache, cached
11
- from pydantic import BaseModel, model_validator
12
10
 
13
11
  from .exceptions import USSOException
14
- from .schemas import UserData
12
+ from .schemas import JWTConfig, UserData
15
13
 
16
14
  logger = logging.getLogger("usso")
17
15
 
@@ -26,36 +24,34 @@ def get_authorization_scheme_param(
26
24
 
27
25
 
28
26
  def decode_token(key, token: str, algorithms=["RS256"], **kwargs) -> dict:
27
+ """Decode a JWT token."""
29
28
  try:
30
29
  decoded = jwt.decode(token, key, algorithms=algorithms)
31
- decoded["data"] = decoded
32
- decoded["token"] = token
30
+ decoded.update({"data": decoded, "token": token})
33
31
  return UserData(**decoded)
34
- except jwt.exceptions.ExpiredSignatureError:
35
- if kwargs.get("raise_exception", True):
36
- raise USSOException(status_code=401, error="expired_signature")
37
- except jwt.exceptions.InvalidSignatureError:
38
- if kwargs.get("raise_exception", True):
39
- raise USSOException(status_code=401, error="invalid_signature")
40
- except jwt.exceptions.InvalidAlgorithmError:
41
- if kwargs.get("raise_exception", True):
42
- raise USSOException(status_code=401, error="invalid_algorithm")
43
- except jwt.exceptions.InvalidIssuedAtError:
44
- if kwargs.get("raise_exception", True):
45
- raise USSOException(status_code=401, error="invalid_issued_at")
46
- except jwt.exceptions.InvalidTokenError:
47
- if kwargs.get("raise_exception", True):
48
- raise USSOException(status_code=401, error="invalid_token")
49
- except jwt.exceptions.InvalidKeyError:
50
- if kwargs.get("raise_exception", True):
51
- raise USSOException(status_code=401, error="invalid_key")
52
- except USSOException as e:
53
- if kwargs.get("raise_exception", True):
54
- raise e
32
+ except jwt.ExpiredSignatureError:
33
+ _handle_exception("expired_signature", **kwargs)
34
+ except jwt.InvalidSignatureError:
35
+ _handle_exception("invalid_signature", **kwargs)
36
+ except jwt.InvalidAlgorithmError:
37
+ _handle_exception("invalid_algorithm", **kwargs)
38
+ except jwt.InvalidIssuedAtError:
39
+ _handle_exception("invalid_issued_at", **kwargs)
40
+ except jwt.InvalidTokenError:
41
+ _handle_exception("invalid_token", **kwargs)
42
+ except jwt.InvalidKeyError:
43
+ _handle_exception("invalid_key", **kwargs)
55
44
  except Exception as e:
56
- if kwargs.get("raise_exception", True):
57
- raise USSOException(status_code=401, error="error", message=str(e))
58
- logger.error(e)
45
+ _handle_exception("error", message=str(e), **kwargs)
46
+
47
+
48
+ def _handle_exception(error_type: str, **kwargs):
49
+ """Handle JWT-related exceptions."""
50
+ if kwargs.get("raise_exception", True):
51
+ raise USSOException(
52
+ status_code=401, error=error_type, message=kwargs.get("message")
53
+ )
54
+ logger.error(kwargs.get("message") or error_type)
59
55
 
60
56
 
61
57
  def is_expired(token: str, **kwargs) -> bool:
@@ -66,69 +62,31 @@ def is_expired(token: str, **kwargs) -> bool:
66
62
  return exp < now
67
63
 
68
64
 
69
- @cached(TTLCache(maxsize=128, ttl=10 * 60))
65
+ @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
70
66
  def get_jwk_keys(jwk_url: str) -> jwt.PyJWKClient:
71
67
  return jwt.PyJWKClient(jwk_url, headers={"User-Agent": "usso-python"})
72
68
 
73
69
 
74
- def decode_token_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
70
+ def decode_token_with_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
75
71
  """Return the user associated with a token value."""
76
72
  try:
77
73
  jwk_client = get_jwk_keys(jwk_url)
78
74
  signing_key = jwk_client.get_signing_key_from_jwt(token)
79
75
  return decode_token(signing_key.key, token, **kwargs)
80
- except USSOException as e:
81
- if kwargs.get("raise_exception", True):
82
- raise e
83
- logger.error(e)
84
76
  except Exception as e:
85
- if kwargs.get("raise_exception", True):
86
- raise USSOException(
87
- status_code=401,
88
- error="error",
89
- message=str(e),
90
- )
91
- logger.error(e)
77
+ _handle_exception("error", message=str(e), **kwargs)
92
78
 
93
79
 
94
- @cached(TTLCache(maxsize=128, ttl=10 * 60))
95
- def get_api_key_data(jwk_url: str, api_key: str):
80
+ @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
81
+ def fetch_api_key_data(jwk_url: str, api_key: str):
96
82
  parsed = urlparse(jwk_url)
97
83
  url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
98
- response = requests.post(url, json={"api_key": api_key})
84
+ response = httpx.post(url, json={"api_key": api_key})
99
85
  response.raise_for_status()
100
86
  return UserData(**response.json())
101
87
 
102
88
 
103
- class JWTConfig(BaseModel):
104
- jwk_url: str | None = None
105
- secret: str | None = None
106
- type: str = "RS256"
107
- header: dict[str, str] = {"type": "Cookie", "name": "usso_access_token"}
108
-
109
- def __hash__(self):
110
- return hash(self.model_dump_json())
111
-
112
- @model_validator(mode="before")
113
- def validate_secret(cls, data: dict):
114
- if not data.get("jwk_url") and not data.get("secret"):
115
- raise ValueError("Either jwk_url or secret must be provided")
116
- return data
117
-
118
- @classmethod
119
- @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
120
- def get_jwk_keys(cls, jwk_url):
121
- return get_jwk_keys(jwk_url)
122
-
123
- @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
124
- def decode(self, token: str):
125
- if self.jwk_url:
126
- return decode_token_jwk(self.jwk_url, token)
127
- return decode_token(self.secret, token, algorithms=[self.type])
128
-
129
-
130
89
  class Usso:
131
-
132
90
  def __init__(
133
91
  self,
134
92
  *,
@@ -138,45 +96,44 @@ class Usso:
138
96
  jwk_url: str | None = None,
139
97
  secret: str | None = None,
140
98
  ):
99
+ self.jwt_configs = self._initialize_configs(jwt_config, jwk_url, secret)
100
+
101
+ def _initialize_configs(
102
+ self,
103
+ jwt_config: (
104
+ str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
105
+ ) = None,
106
+ jwk_url: str | None = None,
107
+ secret: str | None = None,
108
+ ):
109
+ """Initialize JWT configurations."""
141
110
  if jwt_config is None:
142
111
  jwt_config = os.getenv("USSO_JWT_CONFIG")
143
112
 
144
113
  if jwt_config is None:
145
- if not jwk_url:
146
- jwk_url = os.getenv("USSO_JWK_URL") or os.getenv("USSO_JWKS_URL")
114
+ jwk_url = jwk_url or os.getenv("USSO_JWK_URL") or os.getenv("USSO_JWKS_URL")
115
+ secret = secret or os.getenv("USSO_SECRET")
147
116
  if jwk_url:
148
- self.jwt_configs = [JWTConfig(jwk_url=jwk_url)]
149
- return
150
-
151
- if not secret:
152
- secret = os.getenv("USSO_SECRET")
117
+ return [JWTConfig(jwk_url=jwk_url)]
153
118
  if secret:
154
- self.jwt_configs = [JWTConfig(secret=secret)]
155
- return
156
-
119
+ return [JWTConfig(secret=secret)]
157
120
  raise ValueError(
158
- "\n".join(
159
- [
160
- "jwt_config or jwk_url or secret must be provided",
161
- "or set the environment variable USSO_JWT_CONFIG or USSO_JWK_URL or USSO_SECRET",
162
- ]
163
- )
121
+ "Provide jwt_config, jwk_url, or secret, or set the appropriate environment variables."
164
122
  )
165
123
 
166
- def _get_config(jwt_config):
167
- if isinstance(jwt_config, str):
168
- jwt_config = json.loads(jwt_config)
169
- if isinstance(jwt_config, dict):
170
- jwt_config = JWTConfig(**jwt_config)
171
- return jwt_config
124
+ if isinstance(jwt_config, (str, dict, JWTConfig)):
125
+ return [self._parse_config(jwt_config)]
126
+ if isinstance(jwt_config, list):
127
+ return [self._parse_config(config) for config in jwt_config]
128
+ raise ValueError("Invalid jwt_config format")
172
129
 
173
- if isinstance(jwt_config, str | dict | JWTConfig):
174
- jwt_config = [_get_config(jwt_config)]
175
- elif isinstance(jwt_config, list):
176
- jwt_config = [_get_config(j) for j in jwt_config]
177
-
178
- # self.jwk_url = jwt_config
179
- self.jwt_configs = jwt_config
130
+ def _parse_config(self, config):
131
+ """Parse a single JWT configuration."""
132
+ if isinstance(config, str):
133
+ config = json.loads(config)
134
+ if isinstance(config, dict):
135
+ return JWTConfig(**config)
136
+ return config
180
137
 
181
138
  def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
182
139
  """Return the user associated with a token value."""
@@ -185,49 +142,15 @@ class Usso:
185
142
  try:
186
143
  user_data = jwk_config.decode(token)
187
144
  if user_data.token_type.lower() != kwargs.get("token_type", "access"):
188
- raise USSOException(
189
- status_code=401,
190
- error="invalid_token_type",
191
- message="Token type must be 'access'",
192
- )
193
-
145
+ _handle_exception("invalid_token_type", **kwargs)
194
146
  return user_data
195
-
196
147
  except USSOException as e:
197
148
  exp = e
198
149
 
199
150
  if kwargs.get("raise_exception", True):
200
151
  if exp:
201
- raise exp
202
- raise USSOException(
203
- status_code=401,
204
- error="unauthorized",
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
- )
152
+ _handle_exception(exp.error, message=str(exp), **kwargs)
153
+ _handle_exception("unauthorized", **kwargs)
231
154
 
232
155
  def user_data_from_api_key(self, api_key: str):
233
- return get_api_key_data(self.jwt_configs[0].jwk_url, api_key)
156
+ return fetch_api_key_data(self.jwt_configs[0].jwk_url, api_key)
usso/exceptions.py CHANGED
@@ -1,8 +1,9 @@
1
1
  error_messages = {
2
- "invalid_signature": "Invalid signature",
3
- "invalid_token": "Invalid token",
4
- "expired_signature": "Expired signature",
2
+ "invalid_signature": "Unauthorized. The JWT signature is invalid.",
3
+ "invalid_token": "Unauthorized. The JWT is invalid or not provided.",
4
+ "expired_signature": "Unauthorized. The JWT is expired.",
5
5
  "unauthorized": "Unauthorized",
6
+ "invalid_token_type": "Unauthorized. Token type must be 'access'",
6
7
  }
7
8
 
8
9
 
@@ -14,4 +15,3 @@ class USSOException(Exception):
14
15
  if message is None:
15
16
  self.message = error_messages[error]
16
17
  super().__init__(message)
17
-
usso/schemas.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import uuid
2
2
 
3
- from pydantic import BaseModel
3
+ import cachetools.func
4
+ from pydantic import BaseModel, model_validator
4
5
 
5
6
  from . import b64tools
6
7
 
@@ -37,3 +38,30 @@ class UserData(BaseModel):
37
38
  @property
38
39
  def b64id(self) -> uuid.UUID:
39
40
  return b64tools.b64_encode_uuid_strip(self.uid)
41
+
42
+
43
+ class JWTConfig(BaseModel):
44
+ """Configuration for JWT processing."""
45
+
46
+ jwk_url: str | None = None
47
+ secret: str | None = None
48
+ algorithm: str = "RS256"
49
+ header: dict[str, str] = {"type": "Cookie", "name": "usso_access_token"}
50
+
51
+ def __hash__(self):
52
+ return hash(self.model_dump_json())
53
+
54
+ @model_validator(mode="before")
55
+ def validate_config(cls, data: dict):
56
+ if not data.get("jwk_url") and not data.get("secret"):
57
+ raise ValueError("Either jwk_url or secret must be provided")
58
+ return data
59
+
60
+ @cachetools.func.ttl_cache(maxsize=128, ttl=600)
61
+ def decode(self, token: str):
62
+ """Decode a token using the configured method."""
63
+ from .core import decode_token, decode_token_with_jwk
64
+
65
+ if self.jwk_url:
66
+ return decode_token_with_jwk(self.jwk_url, token)
67
+ return decode_token(self.secret, token, algorithms=[self.algorithm])
@@ -1,5 +1,7 @@
1
1
  import os
2
+
2
3
  import httpx
4
+
3
5
  from ..core import is_expired
4
6
  from .base_session import BaseUssoSession
5
7
 
@@ -17,18 +19,16 @@ class AsyncUssoSession(httpx.AsyncClient, BaseUssoSession):
17
19
  client: "AsyncUssoSession" | None = None,
18
20
  ):
19
21
  httpx.AsyncClient.__init__(self)
20
- if client:
21
- self.copy_attributes_from(client)
22
- else:
23
- BaseUssoSession.__init__(
24
- self,
25
- usso_base_url=usso_base_url,
26
- api_key=api_key,
27
- usso_refresh_url=usso_refresh_url,
28
- refresh_token=refresh_token,
29
- usso_api_key=usso_api_key,
30
- user_id=user_id,
31
- )
22
+ BaseUssoSession.__init__(
23
+ self,
24
+ usso_base_url=usso_base_url,
25
+ api_key=api_key,
26
+ usso_refresh_url=usso_refresh_url,
27
+ refresh_token=refresh_token,
28
+ usso_api_key=usso_api_key,
29
+ user_id=user_id,
30
+ client=client,
31
+ )
32
32
  self._refresh_sync()
33
33
 
34
34
  def _prepare_refresh_request(self) -> tuple[dict, dict]:
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from urllib.parse import urlparse
3
+
3
4
  from usso.core import is_expired
4
5
 
5
6
 
@@ -13,7 +14,12 @@ class BaseUssoSession:
13
14
  refresh_token: str | None = os.getenv("USSO_REFRESH_TOKEN"),
14
15
  usso_api_key: str | None = os.getenv("USSO_ADMIN_API_KEY"),
15
16
  user_id: str | None = None,
17
+ client: "BaseUssoSession" | None = None,
16
18
  ):
19
+ if client:
20
+ self.copy_attributes_from(client)
21
+ return
22
+
17
23
  assert (
18
24
  usso_base_url or usso_refresh_url
19
25
  ), "usso_base_url or usso_refresh_url is required"
usso/session/session.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
3
  import httpx
4
+
4
5
  from usso.core import is_expired
5
6
 
6
7
  from .base_session import BaseUssoSession
@@ -21,18 +22,16 @@ class UssoSession(BaseUssoSession, httpx.Client):
21
22
  ):
22
23
  httpx.Client.__init__(self)
23
24
 
24
- if client:
25
- self.copy_attributes_from(client)
26
- else:
27
- BaseUssoSession.__init__(
28
- self,
29
- usso_base_url=usso_base_url,
30
- api_key=api_key,
31
- usso_refresh_url=usso_refresh_url,
32
- refresh_token=refresh_token,
33
- usso_api_key=usso_api_key,
34
- user_id=user_id,
35
- )
25
+ BaseUssoSession.__init__(
26
+ self,
27
+ usso_base_url=usso_base_url,
28
+ api_key=api_key,
29
+ usso_refresh_url=usso_refresh_url,
30
+ refresh_token=refresh_token,
31
+ usso_api_key=usso_api_key,
32
+ user_id=user_id,
33
+ client=client,
34
+ )
36
35
  self._refresh()
37
36
 
38
37
  def _refresh_api(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.27.10
3
+ Version: 0.27.12
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>
@@ -43,9 +43,9 @@ Requires-Python: >=3.9
43
43
  Description-Content-Type: text/markdown
44
44
  License-File: LICENSE.txt
45
45
  Requires-Dist: pydantic>=2
46
- Requires-Dist: requests>=2.26.0
47
46
  Requires-Dist: pyjwt[crypto]
48
47
  Requires-Dist: cachetools
48
+ Requires-Dist: httpx
49
49
  Provides-Extra: fastapi
50
50
  Requires-Dist: fastapi>=0.65.0; extra == "fastapi"
51
51
  Requires-Dist: uvicorn[standard]>=0.13.0; extra == "fastapi"
@@ -1,8 +1,8 @@
1
1
  usso/__init__.py,sha256=NnOS_S1a-JKTOlGe1nw-kCL3m0y82mA2mDraus7BQ2o,120
2
2
  usso/b64tools.py,sha256=HGQ0E59vzjrQo2-4jrcY03ebtTaYwTtCZ7KgJaEmxO0,610
3
- usso/core.py,sha256=akOET-sKPhdFQLdXCi8zDhEI2sBKjsdDqkUy7h0IBTE,8042
4
- usso/exceptions.py,sha256=syb8V6ysKivFk52NyPAkeMSMQkNi0h8yxDnz_uI2VkA,505
5
- usso/schemas.py,sha256=nYFqBMtnGJw13cSKSIMIZdxKVz3AIbnETDuiENHdl5g,850
3
+ usso/core.py,sha256=R4WrOn5bkBJzAnvJZldfihfwNvMn0BnjoKkO5eCnDTA,5533
4
+ usso/exceptions.py,sha256=ogJsjdUK0HoZdQv5uCnzIVoG-bTTMHBqyvB4swAMsiE,653
5
+ usso/schemas.py,sha256=aK_UWZvqjZLz5r1yBIZX_nL2yPCNUjxpZ93AsV9mAes,1810
6
6
  usso/client/__init__.py,sha256=ilGFrugI7bhGXVIcETdbRAye8S7k2mVjkEeziToVzSs,100
7
7
  usso/client/api.py,sha256=xlDq2nZNpq3mhAvqIbGEfANHNjJpPquSeULBfS7iMJw,5094
8
8
  usso/client/async_api.py,sha256=VBmuUsx9vBy-naeiVNhsGgJOTpD1z7VgH_23lazz3_4,5156
@@ -11,12 +11,12 @@ usso/django/middleware.py,sha256=EEEpHvMQ6QiWw2HY8zQ2Aec0RCATcLWsCKeyiPWJKio,324
11
11
  usso/fastapi/__init__.py,sha256=0EcdOzb4f3yu9nILIdGWnlyUz-0VaVX2az1e3f2BusI,201
12
12
  usso/fastapi/integration.py,sha256=IonxxNj_B9sG2j672rIzE047qo972vk7ch4-eGENp3Q,2638
13
13
  usso/session/__init__.py,sha256=tE4qWUdSI7iN_pywm47Mg8NKOTBa2nCNwCy3wCZWRmU,124
14
- usso/session/async_session.py,sha256=Q7Rbw5z5tiiagNEvRR26SP7zCvwtV5DTWnanQWghvms,3577
15
- usso/session/base_session.py,sha256=Hs5ZEME04TJF3qNfk2gqD81Ib9UTZw_ArDRELxnSYVM,2829
16
- usso/session/session.py,sha256=PHVkDACeSCP7h2Pn38SEl905akwyRbk38gk6-KD8fqI,2450
17
- usso-0.27.10.dist-info/LICENSE.txt,sha256=ceC9ZJOV9H6CtQDcYmHOS46NA3dHJ_WD4J9blH513pc,1081
18
- usso-0.27.10.dist-info/METADATA,sha256=7T5zV2MktxkK_Wq-MTHXg0yR0OseSear_9liczD51ks,4529
19
- usso-0.27.10.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
- usso-0.27.10.dist-info/entry_points.txt,sha256=4Zgpm5ELaAWPf0jPGJFz1_X69H7un8ycT3WdGoJ0Vvk,35
21
- usso-0.27.10.dist-info/top_level.txt,sha256=g9Jf6h1Oyidh0vPiFni7UHInTJjSvu6cUalpLTIvthg,5
22
- usso-0.27.10.dist-info/RECORD,,
14
+ usso/session/async_session.py,sha256=7n-Gp7mfW2W4X24qPo8bWSBj4BT1VJ3tfWia9cR7OSA,3491
15
+ usso/session/base_session.py,sha256=rUYKWO9UtDfDAv-WrUPMz99LRgf2ilZ9EGtq6B5NDcA,2964
16
+ usso/session/session.py,sha256=1s9OoaQerAfyfx5QtsU7ubWsnXq0pI-0vCaIn7EFkZU,2363
17
+ usso-0.27.12.dist-info/LICENSE.txt,sha256=ceC9ZJOV9H6CtQDcYmHOS46NA3dHJ_WD4J9blH513pc,1081
18
+ usso-0.27.12.dist-info/METADATA,sha256=qjPFM2HGvS83cTSKZ97ylhJpjz0mnlpjoyGfKoKLKrc,4518
19
+ usso-0.27.12.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
+ usso-0.27.12.dist-info/entry_points.txt,sha256=4Zgpm5ELaAWPf0jPGJFz1_X69H7un8ycT3WdGoJ0Vvk,35
21
+ usso-0.27.12.dist-info/top_level.txt,sha256=g9Jf6h1Oyidh0vPiFni7UHInTJjSvu6cUalpLTIvthg,5
22
+ usso-0.27.12.dist-info/RECORD,,
File without changes