usso 0.24.10__tar.gz → 0.24.12__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 (29) hide show
  1. {usso-0.24.10/src/usso.egg-info → usso-0.24.12}/PKG-INFO +3 -2
  2. {usso-0.24.10 → usso-0.24.12}/pyproject.toml +4 -4
  3. {usso-0.24.10 → usso-0.24.12}/src/usso/async_session.py +34 -4
  4. {usso-0.24.10 → usso-0.24.12}/src/usso/core.py +18 -31
  5. {usso-0.24.10 → usso-0.24.12}/src/usso/fastapi/auth_middleware.py +40 -30
  6. usso-0.24.12/src/usso/session.py +68 -0
  7. {usso-0.24.10 → usso-0.24.12/src/usso.egg-info}/PKG-INFO +3 -2
  8. {usso-0.24.10 → usso-0.24.12}/src/usso.egg-info/requires.txt +2 -1
  9. usso-0.24.10/src/usso/session.py +0 -38
  10. {usso-0.24.10 → usso-0.24.12}/LICENSE.txt +0 -0
  11. {usso-0.24.10 → usso-0.24.12}/README.md +0 -0
  12. {usso-0.24.10 → usso-0.24.12}/setup.cfg +0 -0
  13. {usso-0.24.10 → usso-0.24.12}/src/usso/__init__.py +0 -0
  14. {usso-0.24.10 → usso-0.24.12}/src/usso/api.py +0 -0
  15. {usso-0.24.10 → usso-0.24.12}/src/usso/async_api.py +0 -0
  16. {usso-0.24.10 → usso-0.24.12}/src/usso/b64tools.py +0 -0
  17. {usso-0.24.10 → usso-0.24.12}/src/usso/django/__init__.py +0 -0
  18. {usso-0.24.10 → usso-0.24.12}/src/usso/django/middleware.py +0 -0
  19. {usso-0.24.10 → usso-0.24.12}/src/usso/exceptions.py +0 -0
  20. {usso-0.24.10 → usso-0.24.12}/src/usso/fastapi/__init__.py +0 -0
  21. {usso-0.24.10 → usso-0.24.12}/src/usso/fastapi/integration.py +0 -0
  22. {usso-0.24.10 → usso-0.24.12}/src/usso/package_data.dat +0 -0
  23. {usso-0.24.10 → usso-0.24.12}/src/usso.egg-info/SOURCES.txt +0 -0
  24. {usso-0.24.10 → usso-0.24.12}/src/usso.egg-info/dependency_links.txt +0 -0
  25. {usso-0.24.10 → usso-0.24.12}/src/usso.egg-info/entry_points.txt +0 -0
  26. {usso-0.24.10 → usso-0.24.12}/src/usso.egg-info/top_level.txt +0 -0
  27. {usso-0.24.10 → usso-0.24.12}/tests/test_api.py +0 -0
  28. {usso-0.24.10 → usso-0.24.12}/tests/test_core.py +0 -0
  29. {usso-0.24.10 → usso-0.24.12}/tests/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.24.10
3
+ Version: 0.24.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,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.10"
7
+ version = "0.24.12"
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,21 +1,51 @@
1
1
  from contextlib import asynccontextmanager
2
- from datetime import datetime
2
+ from datetime import datetime, timedelta
3
3
 
4
4
  import aiohttp
5
5
  import jwt
6
6
 
7
7
 
8
8
  class AsyncUssoSession:
9
- def __init__(self, sso_refresh_url: str, refresh_token: str | None = None):
9
+ def __init__(
10
+ self,
11
+ sso_refresh_url: str,
12
+ refresh_token: str | None = None,
13
+ api_key: str | None = None,
14
+ ):
10
15
  self.sso_refresh_url = sso_refresh_url
11
- self.refresh_token = refresh_token
16
+ self._refresh_token = refresh_token
12
17
  self.access_token = None
13
18
  self.session = None # This will hold the aiohttp session
19
+ self.api_key = api_key
20
+
21
+ @property
22
+ def refresh_token(self):
23
+ decoded_token = jwt.decode(
24
+ self._refresh_token, options={"verify_signature": False}
25
+ )
26
+ exp = decoded_token.get("exp", datetime.now() + timedelta(days=1))
27
+ if exp < datetime.now():
28
+ self._refresh_token = None
29
+
30
+ return self._refresh_token
31
+
32
+ async def _refresh_api(self):
33
+ async with aiohttp.ClientSession() as session:
34
+ async with session.get(
35
+ f"{self.sso_refresh_url}/api",
36
+ headers={"x-api-key": self.api_key},
37
+ ) as response:
38
+ response.raise_for_status()
39
+ data: dict = await response.json()
40
+ self._refresh_token = data.get("token", {}).get("refresh_token")
14
41
 
15
42
  async def _refresh(self):
16
- if not self.refresh_token:
43
+ if not self.refresh_token and not self.api_key:
17
44
  raise ValueError("Refresh token not provided or invalid.")
18
45
 
46
+ if self.api_key and not self.refresh_token:
47
+ self._refresh_api()
48
+
19
49
  async with aiohttp.ClientSession() as session:
20
50
  async with session.post(
21
51
  self.sso_refresh_url,
@@ -64,23 +64,11 @@ class Usso(metaclass=Singleton):
64
64
  scheme, _, param = authorization_header_value.partition(" ")
65
65
  return scheme, param
66
66
 
67
- def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
68
- """Return the user associated with a token value."""
67
+ def decode_token(self, key, token: str, **kwargs) -> dict:
69
68
  try:
70
- # header = jwt.get_unverified_header(token)
71
- # jwks_url = header["jwk_url"]
72
- jwks_client = self.get_jwks_keys()
73
- signing_key = jwks_client.get_signing_key_from_jwt(token)
74
- decoded = jwt.decode(
75
- token,
76
- signing_key.key,
77
- algorithms=["RS256"],
78
- )
69
+ decoded = jwt.decode(token, key, algorithms=["RS256"])
79
70
  if decoded["token_type"] != "access":
80
- raise USSOException(
81
- status_code=401,
82
- error="invalid_token_type",
83
- )
71
+ raise USSOException(status_code=401, error="invalid_token_type")
84
72
  decoded["token"] = token
85
73
  return UserData(**decoded)
86
74
  except jwt.exceptions.ExpiredSignatureError:
@@ -91,31 +79,30 @@ class Usso(metaclass=Singleton):
91
79
  raise USSOException(status_code=401, error="invalid_signature")
92
80
  except jwt.exceptions.InvalidAlgorithmError:
93
81
  if kwargs.get("raise_exception", True):
94
- raise USSOException(
95
- status_code=401,
96
- error="invalid_algorithm",
97
- )
82
+ raise USSOException(status_code=401, error="invalid_algorithm")
98
83
  except jwt.exceptions.InvalidIssuedAtError:
99
84
  if kwargs.get("raise_exception", True):
100
- raise USSOException(
101
- status_code=401,
102
- error="invalid_issued_at",
103
- )
85
+ raise USSOException(status_code=401, error="invalid_issued_at")
104
86
  except jwt.exceptions.InvalidTokenError:
105
87
  if kwargs.get("raise_exception", True):
106
- raise USSOException(
107
- status_code=401,
108
- error="invalid_token",
109
- )
88
+ raise USSOException(status_code=401, error="invalid_token")
110
89
  except jwt.exceptions.InvalidKeyError:
111
90
  if kwargs.get("raise_exception", True):
112
- raise USSOException(
113
- status_code=401,
114
- error="invalid_key",
115
- )
91
+ raise USSOException(status_code=401, error="invalid_key")
116
92
  except USSOException as e:
117
93
  if kwargs.get("raise_exception", True):
118
94
  raise e
95
+ except Exception as e:
96
+ if kwargs.get("raise_exception", True):
97
+ raise USSOException(status_code=401, error="error", message=str(e))
98
+ logger.error(e)
99
+
100
+ def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
101
+ """Return the user associated with a token value."""
102
+ try:
103
+ jwks_client = self.get_jwks_keys()
104
+ signing_key = jwks_client.get_signing_key_from_jwt(token)
105
+ return self.decode_token(signing_key.key, token, **kwargs)
119
106
  except Exception as e:
120
107
  if kwargs.get("raise_exception", True):
121
108
  raise USSOException(
@@ -73,15 +73,45 @@ class Usso:
73
73
  scheme, _, param = authorization_header_value.partition(" ")
74
74
  return scheme, param
75
75
 
76
+ def decode_token(self, key, token: str, **kwargs) -> dict:
77
+ try:
78
+ decoded = jwt.decode(token, key, algorithms=["RS256"])
79
+ if decoded["token_type"] != "access":
80
+ raise USSOException(status_code=401, error="invalid_token_type")
81
+ decoded["token"] = token
82
+ return UserData(**decoded)
83
+ except jwt.exceptions.ExpiredSignatureError:
84
+ if kwargs.get("raise_exception", True):
85
+ raise USSOException(status_code=401, error="expired_signature")
86
+ except jwt.exceptions.InvalidSignatureError:
87
+ if kwargs.get("raise_exception", True):
88
+ raise USSOException(status_code=401, error="invalid_signature")
89
+ except jwt.exceptions.InvalidAlgorithmError:
90
+ if kwargs.get("raise_exception", True):
91
+ raise USSOException(status_code=401, error="invalid_algorithm")
92
+ except jwt.exceptions.InvalidIssuedAtError:
93
+ if kwargs.get("raise_exception", True):
94
+ raise USSOException(status_code=401, error="invalid_issued_at")
95
+ except jwt.exceptions.InvalidTokenError:
96
+ if kwargs.get("raise_exception", True):
97
+ raise USSOException(status_code=401, error="invalid_token")
98
+ except jwt.exceptions.InvalidKeyError:
99
+ if kwargs.get("raise_exception", True):
100
+ raise USSOException(status_code=401, error="invalid_key")
101
+ except USSOException as e:
102
+ if kwargs.get("raise_exception", True):
103
+ raise e
104
+ except Exception as e:
105
+ if kwargs.get("raise_exception", True):
106
+ raise USSOException(status_code=401, error="error", message=str(e))
107
+ logger.error(e)
108
+
76
109
  def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
77
110
  """Return the user associated with a token value."""
78
111
  try:
79
112
  decoded = self.jwk_url.decode(token)
80
113
  if decoded["token_type"] != "access":
81
- raise USSOException(
82
- status_code=401,
83
- error="invalid_token_type",
84
- )
114
+ raise USSOException(status_code=401, error="invalid_token_type")
85
115
  decoded["token"] = token
86
116
  return UserData(**decoded)
87
117
  except jwt.exceptions.ExpiredSignatureError:
@@ -92,45 +122,25 @@ class Usso:
92
122
  raise USSOException(status_code=401, error="invalid_signature")
93
123
  except jwt.exceptions.InvalidAlgorithmError:
94
124
  if kwargs.get("raise_exception", True):
95
- raise USSOException(
96
- status_code=401,
97
- error="invalid_algorithm",
98
- )
125
+ raise USSOException(status_code=401, error="invalid_algorithm")
99
126
  except jwt.exceptions.InvalidIssuedAtError:
100
127
  if kwargs.get("raise_exception", True):
101
- raise USSOException(
102
- status_code=401,
103
- error="invalid_issued_at",
104
- )
128
+ raise USSOException(status_code=401, error="invalid_issued_at")
105
129
  except jwt.exceptions.InvalidTokenError:
106
130
  if kwargs.get("raise_exception", True):
107
- raise USSOException(
108
- status_code=401,
109
- error="invalid_token",
110
- )
131
+ raise USSOException(status_code=401, error="invalid_token")
111
132
  except jwt.exceptions.InvalidKeyError:
112
133
  if kwargs.get("raise_exception", True):
113
- raise USSOException(
114
- status_code=401,
115
- error="invalid_key",
116
- )
134
+ raise USSOException(status_code=401, error="invalid_key")
117
135
  except KeyError as e:
118
136
  if kwargs.get("raise_exception", True):
119
- raise USSOException(
120
- status_code=401,
121
- error="key_error",
122
- message=str(e),
123
- )
137
+ raise USSOException(status_code=401, error="key_error", message=str(e))
124
138
  except USSOException as e:
125
139
  if kwargs.get("raise_exception", True):
126
140
  raise e
127
141
  except Exception as e:
128
142
  if kwargs.get("raise_exception", True):
129
- raise USSOException(
130
- status_code=401,
131
- error="error",
132
- message=str(e),
133
- )
143
+ raise USSOException(status_code=401, error="error", message=str(e))
134
144
  logger.error(e)
135
145
 
136
146
  async def jwt_access_security(self, request: Request) -> UserData | None:
@@ -0,0 +1,68 @@
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
+ ):
15
+ self.sso_refresh_url = sso_refresh_url
16
+ self._refresh_token = refresh_token
17
+ self.session = requests.Session()
18
+ self.access_token = None
19
+ self.api_key = api_key
20
+
21
+ @property
22
+ def refresh_token(self):
23
+ decoded_token = jwt.decode(
24
+ self._refresh_token, options={"verify_signature": False}
25
+ )
26
+ exp = decoded_token.get("exp", datetime.now() + timedelta(days=1))
27
+ if exp < datetime.now():
28
+ self._refresh_token = None
29
+
30
+ return self._refresh_token
31
+
32
+ def _refresh_api(self):
33
+ response = requests.get(
34
+ f"{self.sso_refresh_url}/api",
35
+ headers={"x-api-key": self.api_key},
36
+ )
37
+ response.raise_for_status()
38
+ data = response.json()
39
+ self._refresh_token = data.get("token", {}).get("refresh_token")
40
+
41
+ def _refresh(self):
42
+ if not self.refresh_token and not self.api_key:
43
+ return
44
+
45
+ if self.api_key and not self.refresh_token:
46
+ self._refresh_api()
47
+
48
+ response = requests.post(
49
+ self.sso_refresh_url,
50
+ json={"refresh_token": f"{self.refresh_token}"},
51
+ )
52
+ response.raise_for_status()
53
+ return response.json()
54
+
55
+ def get_session(self):
56
+ if self.access_token:
57
+ decoded_token = jwt.decode(
58
+ self.access_token, options={"verify_signature": False}
59
+ )
60
+ exp = datetime.fromtimestamp(decoded_token.get("exp"))
61
+ if exp < datetime.now():
62
+ self.access_token = None
63
+ if not self.access_token:
64
+ self.access_token = self._refresh()["access_token"]
65
+ self.session.headers.update(
66
+ {"Authorization": f"Bearer {self.access_token}"}
67
+ )
68
+ return self.session
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.24.10
3
+ Version: 0.24.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,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
@@ -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
@@ -1,38 +0,0 @@
1
- from datetime import datetime
2
-
3
- import jwt
4
- import requests
5
-
6
-
7
- class UssoSession:
8
- def __init__(self, sso_refresh_url: str, refresh_token: str | None = None):
9
- self.sso_refresh_url = sso_refresh_url
10
- self.refresh_token = refresh_token
11
- self.session = requests.Session()
12
- self.access_token = None
13
-
14
- def _refresh(self):
15
- if not self.refresh_token:
16
- return
17
-
18
- response = requests.post(
19
- self.sso_refresh_url,
20
- json={"refresh_token": f"{self.refresh_token}"},
21
- )
22
- response.raise_for_status()
23
- return response.json()
24
-
25
- def get_session(self):
26
- if self.access_token:
27
- decoded_token = jwt.decode(
28
- self.access_token, options={"verify_signature": False}
29
- )
30
- exp = datetime.fromtimestamp(decoded_token.get("exp"))
31
- if exp < datetime.now():
32
- self.access_token = None
33
- if not self.access_token:
34
- self.access_token = self._refresh()["access_token"]
35
- self.session.headers.update(
36
- {"Authorization": f"Bearer {self.access_token}"}
37
- )
38
- return self.session
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