usso 0.24.9__tar.gz → 0.24.11__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 (28) hide show
  1. {usso-0.24.9/src/usso.egg-info → usso-0.24.11}/PKG-INFO +3 -2
  2. {usso-0.24.9 → usso-0.24.11}/pyproject.toml +4 -4
  3. {usso-0.24.9 → usso-0.24.11}/src/usso/async_session.py +2 -1
  4. usso-0.24.11/src/usso/fastapi/auth_middleware.py +176 -0
  5. {usso-0.24.9 → usso-0.24.11/src/usso.egg-info}/PKG-INFO +3 -2
  6. {usso-0.24.9 → usso-0.24.11}/src/usso.egg-info/SOURCES.txt +1 -0
  7. {usso-0.24.9 → usso-0.24.11}/src/usso.egg-info/requires.txt +2 -1
  8. {usso-0.24.9 → usso-0.24.11}/LICENSE.txt +0 -0
  9. {usso-0.24.9 → usso-0.24.11}/README.md +0 -0
  10. {usso-0.24.9 → usso-0.24.11}/setup.cfg +0 -0
  11. {usso-0.24.9 → usso-0.24.11}/src/usso/__init__.py +0 -0
  12. {usso-0.24.9 → usso-0.24.11}/src/usso/api.py +0 -0
  13. {usso-0.24.9 → usso-0.24.11}/src/usso/async_api.py +0 -0
  14. {usso-0.24.9 → usso-0.24.11}/src/usso/b64tools.py +0 -0
  15. {usso-0.24.9 → usso-0.24.11}/src/usso/core.py +0 -0
  16. {usso-0.24.9 → usso-0.24.11}/src/usso/django/__init__.py +0 -0
  17. {usso-0.24.9 → usso-0.24.11}/src/usso/django/middleware.py +0 -0
  18. {usso-0.24.9 → usso-0.24.11}/src/usso/exceptions.py +0 -0
  19. {usso-0.24.9 → usso-0.24.11}/src/usso/fastapi/__init__.py +0 -0
  20. {usso-0.24.9 → usso-0.24.11}/src/usso/fastapi/integration.py +0 -0
  21. {usso-0.24.9 → usso-0.24.11}/src/usso/package_data.dat +0 -0
  22. {usso-0.24.9 → usso-0.24.11}/src/usso/session.py +0 -0
  23. {usso-0.24.9 → usso-0.24.11}/src/usso.egg-info/dependency_links.txt +0 -0
  24. {usso-0.24.9 → usso-0.24.11}/src/usso.egg-info/entry_points.txt +0 -0
  25. {usso-0.24.9 → usso-0.24.11}/src/usso.egg-info/top_level.txt +0 -0
  26. {usso-0.24.9 → usso-0.24.11}/tests/test_api.py +0 -0
  27. {usso-0.24.9 → usso-0.24.11}/tests/test_core.py +0 -0
  28. {usso-0.24.9 → usso-0.24.11}/tests/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.24.9
3
+ Version: 0.24.11
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,13 +43,14 @@ Requires-Python: >=3.9
43
43
  Description-Content-Type: text/markdown
44
44
  License-File: LICENSE.txt
45
45
  Requires-Dist: peppercorn
46
- Requires-Dist: pydantic>=1.8.2
46
+ Requires-Dist: pydantic>=2
47
47
  Requires-Dist: requests>=2.26.0
48
48
  Requires-Dist: pyjwt[crypto]
49
49
  Requires-Dist: singleton_package
50
50
  Provides-Extra: fastapi
51
51
  Requires-Dist: fastapi>=0.65.0; extra == "fastapi"
52
52
  Requires-Dist: uvicorn[standard]>=0.13.0; extra == "fastapi"
53
+ Requires-Dist: cachetools; extra == "fastapi"
53
54
  Provides-Extra: django
54
55
  Requires-Dist: Django>=3.2; extra == "django"
55
56
  Provides-Extra: dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "usso"
7
- version = "0.24.9"
7
+ version = "0.24.11"
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"
@@ -29,12 +29,12 @@ classifiers = [
29
29
  ]
30
30
  dependencies = [
31
31
  "peppercorn", # Example main dependency
32
- "pydantic>=1.8.2",
32
+ "pydantic>=2",
33
33
  "requests>=2.26.0",
34
34
  "pyjwt[crypto]",
35
- "singleton_package"
35
+ "singleton_package",
36
36
  ]
37
- optional-dependencies = {"fastapi" = ["fastapi>=0.65.0", "uvicorn[standard]>=0.13.0"],"django" = ["Django>=3.2"],"dev" = ["check-manifest"],"test" = ["coverage"]}
37
+ optional-dependencies = {"fastapi" = ["fastapi>=0.65.0", "uvicorn[standard]>=0.13.0", "cachetools"],"django" = ["Django>=3.2"],"dev" = ["check-manifest"],"test" = ["coverage"]}
38
38
 
39
39
  [project.urls]
40
40
  "Homepage" = "https://github.com/ussoio/usso-python"
@@ -1,7 +1,8 @@
1
1
  from contextlib import asynccontextmanager
2
+ from datetime import datetime
3
+
2
4
  import aiohttp
3
5
  import jwt
4
- from datetime import datetime
5
6
 
6
7
 
7
8
  class AsyncUssoSession:
@@ -0,0 +1,176 @@
1
+ import json
2
+ import logging
3
+ import os
4
+
5
+ import cachetools.func
6
+ import jwt
7
+ from fastapi import Request, WebSocket
8
+ from pydantic import BaseModel, model_validator
9
+ from starlette.status import HTTP_401_UNAUTHORIZED
10
+
11
+ from usso.core import UserData
12
+ from usso.exceptions import USSOException
13
+
14
+ logger = logging.getLogger("usso")
15
+
16
+
17
+ class JWTConfig(BaseModel):
18
+ jwk_url: str | None = None
19
+ secret: str | None = None
20
+ type: str = "RS256"
21
+ header: dict[str, str] = {"type": "Cookie", "name": "usso_access_token"}
22
+
23
+ def __hash__(self):
24
+ return hash(self.model_dump_json())
25
+
26
+ @model_validator(mode="before")
27
+ def validate_secret(cls, data: dict):
28
+ if not data.get("jwk_url") and not data.get("secret"):
29
+ raise ValueError("Either jwk_url or secret must be provided")
30
+ return data
31
+
32
+ @classmethod
33
+ @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
34
+ def get_jwk_keys(cls, jwk_url):
35
+ return jwt.PyJWKClient(
36
+ jwk_url,
37
+ headers={
38
+ "User-Agent": "usso-python",
39
+ },
40
+ )
41
+
42
+ @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
43
+ def decode(self, token: str):
44
+ if self.jwk_url:
45
+ jwk_client = self.get_jwk_keys(self.jwk_url)
46
+ signing_key = jwk_client.get_signing_key_from_jwt(token)
47
+ return jwt.decode(token, signing_key.key, algorithms=[self.type])
48
+
49
+ return jwt.decode(token, self.secret, algorithms=[self.type])
50
+
51
+
52
+ class Usso:
53
+
54
+ def __init__(self, jwt_config: str | dict | JWTConfig | None = None):
55
+ if jwt_config is None:
56
+ self.jwk_url = os.getenv("USSO_JWK_URL")
57
+ return
58
+
59
+ if isinstance(jwt_config, str):
60
+ jwt_config = json.loads(jwt_config)
61
+ if isinstance(jwt_config, dict):
62
+ jwt_config = JWTConfig(**jwt_config)
63
+
64
+ self.jwk_url = jwt_config
65
+ self.jwt_config = jwt_config
66
+
67
+ def get_authorization_scheme_param(
68
+ self,
69
+ authorization_header_value: str | None,
70
+ ) -> tuple[str, str]:
71
+ if not authorization_header_value:
72
+ return "", ""
73
+ scheme, _, param = authorization_header_value.partition(" ")
74
+ return scheme, param
75
+
76
+ def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
77
+ """Return the user associated with a token value."""
78
+ try:
79
+ decoded = self.jwk_url.decode(token)
80
+ if decoded["token_type"] != "access":
81
+ raise USSOException(
82
+ status_code=401,
83
+ error="invalid_token_type",
84
+ )
85
+ decoded["token"] = token
86
+ return UserData(**decoded)
87
+ except jwt.exceptions.ExpiredSignatureError:
88
+ if kwargs.get("raise_exception", True):
89
+ raise USSOException(status_code=401, error="expired_signature")
90
+ except jwt.exceptions.InvalidSignatureError:
91
+ if kwargs.get("raise_exception", True):
92
+ raise USSOException(status_code=401, error="invalid_signature")
93
+ except jwt.exceptions.InvalidAlgorithmError:
94
+ if kwargs.get("raise_exception", True):
95
+ raise USSOException(
96
+ status_code=401,
97
+ error="invalid_algorithm",
98
+ )
99
+ except jwt.exceptions.InvalidIssuedAtError:
100
+ if kwargs.get("raise_exception", True):
101
+ raise USSOException(
102
+ status_code=401,
103
+ error="invalid_issued_at",
104
+ )
105
+ except jwt.exceptions.InvalidTokenError:
106
+ if kwargs.get("raise_exception", True):
107
+ raise USSOException(
108
+ status_code=401,
109
+ error="invalid_token",
110
+ )
111
+ except jwt.exceptions.InvalidKeyError:
112
+ if kwargs.get("raise_exception", True):
113
+ raise USSOException(
114
+ status_code=401,
115
+ error="invalid_key",
116
+ )
117
+ except KeyError as e:
118
+ if kwargs.get("raise_exception", True):
119
+ raise USSOException(
120
+ status_code=401,
121
+ error="key_error",
122
+ message=str(e),
123
+ )
124
+ except USSOException as e:
125
+ if kwargs.get("raise_exception", True):
126
+ raise e
127
+ except Exception as e:
128
+ if kwargs.get("raise_exception", True):
129
+ raise USSOException(
130
+ status_code=401,
131
+ error="error",
132
+ message=str(e),
133
+ )
134
+ logger.error(e)
135
+
136
+ async def jwt_access_security(self, request: Request) -> UserData | None:
137
+ """Return the user associated with a token value."""
138
+ kwargs = {}
139
+ authorization = request.headers.get("Authorization")
140
+ if authorization:
141
+ scheme, credentials = self.get_authorization_scheme_param(authorization)
142
+ if scheme.lower() == "bearer":
143
+ token = credentials
144
+ return self.user_data_from_token(token, **kwargs)
145
+
146
+ cookie_token = request.cookies.get("usso_access_token")
147
+ if cookie_token:
148
+ return self.user_data_from_token(cookie_token, **kwargs)
149
+
150
+ if kwargs.get("raise_exception", True):
151
+ raise USSOException(
152
+ status_code=HTTP_401_UNAUTHORIZED,
153
+ error="unauthorized",
154
+ )
155
+ return None
156
+
157
+ async def jwt_access_security_ws(self, websocket: WebSocket) -> UserData | None:
158
+ """Return the user associated with a token value."""
159
+ kwargs = {}
160
+ authorization = websocket.headers.get("Authorization")
161
+ if authorization:
162
+ scheme, credentials = self.get_authorization_scheme_param(authorization)
163
+ if scheme.lower() == "bearer":
164
+ token = credentials
165
+ return self.user_data_from_token(token, **kwargs)
166
+
167
+ cookie_token = websocket.cookies.get("usso_access_token")
168
+ if cookie_token:
169
+ return self.user_data_from_token(cookie_token, **kwargs)
170
+
171
+ if kwargs.get("raise_exception", True):
172
+ raise USSOException(
173
+ status_code=HTTP_401_UNAUTHORIZED,
174
+ error="unauthorized",
175
+ )
176
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.24.9
3
+ Version: 0.24.11
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,13 +43,14 @@ Requires-Python: >=3.9
43
43
  Description-Content-Type: text/markdown
44
44
  License-File: LICENSE.txt
45
45
  Requires-Dist: peppercorn
46
- Requires-Dist: pydantic>=1.8.2
46
+ Requires-Dist: pydantic>=2
47
47
  Requires-Dist: requests>=2.26.0
48
48
  Requires-Dist: pyjwt[crypto]
49
49
  Requires-Dist: singleton_package
50
50
  Provides-Extra: fastapi
51
51
  Requires-Dist: fastapi>=0.65.0; extra == "fastapi"
52
52
  Requires-Dist: uvicorn[standard]>=0.13.0; extra == "fastapi"
53
+ Requires-Dist: cachetools; extra == "fastapi"
53
54
  Provides-Extra: django
54
55
  Requires-Dist: Django>=3.2; extra == "django"
55
56
  Provides-Extra: dev
@@ -19,6 +19,7 @@ src/usso.egg-info/top_level.txt
19
19
  src/usso/django/__init__.py
20
20
  src/usso/django/middleware.py
21
21
  src/usso/fastapi/__init__.py
22
+ src/usso/fastapi/auth_middleware.py
22
23
  src/usso/fastapi/integration.py
23
24
  tests/test_api.py
24
25
  tests/test_core.py
@@ -1,5 +1,5 @@
1
1
  peppercorn
2
- pydantic>=1.8.2
2
+ pydantic>=2
3
3
  requests>=2.26.0
4
4
  pyjwt[crypto]
5
5
  singleton_package
@@ -13,6 +13,7 @@ Django>=3.2
13
13
  [fastapi]
14
14
  fastapi>=0.65.0
15
15
  uvicorn[standard]>=0.13.0
16
+ cachetools
16
17
 
17
18
  [test]
18
19
  coverage
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