usso 0.27.11__tar.gz → 0.27.13__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 (32) hide show
  1. {usso-0.27.11/src/usso.egg-info → usso-0.27.13}/PKG-INFO +1 -1
  2. {usso-0.27.11 → usso-0.27.13}/pyproject.toml +1 -1
  3. usso-0.27.13/src/usso/core.py +156 -0
  4. {usso-0.27.11 → usso-0.27.13}/src/usso/exceptions.py +4 -4
  5. usso-0.27.13/src/usso/schemas.py +67 -0
  6. {usso-0.27.11 → usso-0.27.13}/src/usso/session/async_session.py +12 -12
  7. {usso-0.27.11 → usso-0.27.13}/src/usso/session/base_session.py +6 -0
  8. {usso-0.27.11 → usso-0.27.13}/src/usso/session/session.py +12 -13
  9. {usso-0.27.11 → usso-0.27.13/src/usso.egg-info}/PKG-INFO +1 -1
  10. {usso-0.27.11 → usso-0.27.13}/src/usso.egg-info/SOURCES.txt +1 -4
  11. usso-0.27.11/src/usso/core.py +0 -233
  12. usso-0.27.11/src/usso/schemas.py +0 -39
  13. usso-0.27.11/tests/test_api.py +0 -61
  14. usso-0.27.11/tests/test_core.py +0 -81
  15. usso-0.27.11/tests/test_simple.py +0 -17
  16. {usso-0.27.11 → usso-0.27.13}/LICENSE.txt +0 -0
  17. {usso-0.27.11 → usso-0.27.13}/README.md +0 -0
  18. {usso-0.27.11 → usso-0.27.13}/setup.cfg +0 -0
  19. {usso-0.27.11 → usso-0.27.13}/src/usso/__init__.py +0 -0
  20. {usso-0.27.11 → usso-0.27.13}/src/usso/b64tools.py +0 -0
  21. {usso-0.27.11 → usso-0.27.13}/src/usso/client/__init__.py +0 -0
  22. {usso-0.27.11 → usso-0.27.13}/src/usso/client/api.py +0 -0
  23. {usso-0.27.11 → usso-0.27.13}/src/usso/client/async_api.py +0 -0
  24. {usso-0.27.11 → usso-0.27.13}/src/usso/django/__init__.py +0 -0
  25. {usso-0.27.11 → usso-0.27.13}/src/usso/django/middleware.py +0 -0
  26. {usso-0.27.11 → usso-0.27.13}/src/usso/fastapi/__init__.py +0 -0
  27. {usso-0.27.11 → usso-0.27.13}/src/usso/fastapi/integration.py +0 -0
  28. {usso-0.27.11 → usso-0.27.13}/src/usso/session/__init__.py +0 -0
  29. {usso-0.27.11 → usso-0.27.13}/src/usso.egg-info/dependency_links.txt +0 -0
  30. {usso-0.27.11 → usso-0.27.13}/src/usso.egg-info/entry_points.txt +0 -0
  31. {usso-0.27.11 → usso-0.27.13}/src/usso.egg-info/requires.txt +0 -0
  32. {usso-0.27.11 → usso-0.27.13}/src/usso.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: usso
3
- Version: 0.27.11
3
+ Version: 0.27.13
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.27.11"
7
+ version = "0.27.13"
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"
@@ -0,0 +1,156 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from datetime import datetime, timedelta
5
+ from urllib.parse import urlparse
6
+
7
+ import cachetools.func
8
+ import httpx
9
+ import jwt
10
+
11
+ from .exceptions import USSOException
12
+ from .schemas import JWTConfig, UserData
13
+
14
+ logger = logging.getLogger("usso")
15
+
16
+
17
+ def get_authorization_scheme_param(
18
+ authorization_header_value: str | None,
19
+ ) -> tuple[str, str]:
20
+ if not authorization_header_value:
21
+ return "", ""
22
+ scheme, _, param = authorization_header_value.partition(" ")
23
+ return scheme, param
24
+
25
+
26
+ def decode_token(key, token: str, algorithms=["RS256"], **kwargs) -> dict:
27
+ """Decode a JWT token."""
28
+ try:
29
+ decoded = jwt.decode(token, key, algorithms=algorithms)
30
+ decoded.update({"data": decoded, "token": token})
31
+ return UserData(**decoded)
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)
44
+ except Exception as 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)
55
+
56
+
57
+ def is_expired(token: str, **kwargs) -> bool:
58
+ now = datetime.now()
59
+ decoded_token: dict = jwt.decode(token, options={"verify_signature": False})
60
+ exp = decoded_token.get("exp", (now + timedelta(days=1)).timestamp())
61
+ exp = datetime.fromtimestamp(exp)
62
+ return exp < now
63
+
64
+
65
+ @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
66
+ def get_jwk_keys(jwk_url: str) -> jwt.PyJWKClient:
67
+ return jwt.PyJWKClient(jwk_url, headers={"User-Agent": "usso-python"})
68
+
69
+
70
+ def decode_token_with_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
71
+ """Return the user associated with a token value."""
72
+ try:
73
+ jwk_client = get_jwk_keys(jwk_url)
74
+ signing_key = jwk_client.get_signing_key_from_jwt(token)
75
+ return decode_token(signing_key.key, token, **kwargs)
76
+ except Exception as e:
77
+ _handle_exception("error", message=str(e), **kwargs)
78
+
79
+
80
+ @cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
81
+ def fetch_api_key_data(jwk_url: str, api_key: str):
82
+ parsed = urlparse(jwk_url)
83
+ url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
84
+ response = httpx.post(url, json={"api_key": api_key})
85
+ response.raise_for_status()
86
+ return UserData(**response.json())
87
+
88
+
89
+ class Usso:
90
+ def __init__(
91
+ self,
92
+ *,
93
+ jwt_config: (
94
+ str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
95
+ ) = None,
96
+ jwk_url: str | None = None,
97
+ secret: str | None = None,
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."""
110
+ if jwt_config is None:
111
+ jwt_config = os.getenv("USSO_JWT_CONFIG")
112
+
113
+ if jwt_config is None:
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")
116
+ if jwk_url:
117
+ return [JWTConfig(jwk_url=jwk_url)]
118
+ if secret:
119
+ return [JWTConfig(secret=secret)]
120
+ raise ValueError(
121
+ "Provide jwt_config, jwk_url, or secret, or set the appropriate environment variables."
122
+ )
123
+
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")
129
+
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
137
+
138
+ def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
139
+ """Return the user associated with a token value."""
140
+ exp = None
141
+ for jwk_config in self.jwt_configs:
142
+ try:
143
+ user_data = jwk_config.decode(token)
144
+ if user_data.token_type.lower() != kwargs.get("token_type", "access"):
145
+ _handle_exception("invalid_token_type", **kwargs)
146
+ return user_data
147
+ except USSOException as e:
148
+ exp = e
149
+
150
+ if kwargs.get("raise_exception", True):
151
+ if exp:
152
+ _handle_exception(exp.error, message=str(exp), **kwargs)
153
+ _handle_exception("unauthorized", **kwargs)
154
+
155
+ def user_data_from_api_key(self, api_key: str):
156
+ return fetch_api_key_data(self.jwt_configs[0].jwk_url, api_key)
@@ -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
-
@@ -0,0 +1,67 @@
1
+ import uuid
2
+
3
+ import cachetools.func
4
+ from pydantic import BaseModel, model_validator
5
+
6
+ from . import b64tools
7
+
8
+
9
+ class UserData(BaseModel):
10
+ user_id: str
11
+ workspace_id: str | None = None
12
+ workspace_ids: list[str] = []
13
+ token_type: str = "access"
14
+
15
+ email: str | None = None
16
+ phone: str | None = None
17
+ username: str | None = None
18
+
19
+ authentication_method: str | None = None
20
+ is_active: bool = False
21
+
22
+ jti: str | None = None
23
+ data: dict | None = None
24
+
25
+ token: str | None = None
26
+
27
+ @property
28
+ def uid(self) -> uuid.UUID:
29
+ user_id = self.user_id
30
+
31
+ if user_id.startswith("u_"):
32
+ user_id = user_id[2:]
33
+ if 22 <= len(user_id) <= 24:
34
+ user_id = b64tools.b64_decode_uuid(user_id)
35
+
36
+ return uuid.UUID(user_id)
37
+
38
+ @property
39
+ def b64id(self) -> uuid.UUID:
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"
@@ -1,12 +1,13 @@
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
7
8
 
8
9
 
9
- class UssoSession(BaseUssoSession, httpx.Client):
10
+ class UssoSession(httpx.Client, BaseUssoSession):
10
11
 
11
12
  def __init__(
12
13
  self,
@@ -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.11
3
+ Version: 0.27.13
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>
@@ -22,7 +22,4 @@ src/usso/fastapi/integration.py
22
22
  src/usso/session/__init__.py
23
23
  src/usso/session/async_session.py
24
24
  src/usso/session/base_session.py
25
- src/usso/session/session.py
26
- tests/test_api.py
27
- tests/test_core.py
28
- tests/test_simple.py
25
+ src/usso/session/session.py
@@ -1,233 +0,0 @@
1
- import json
2
- import logging
3
- import os
4
- from datetime import datetime, timedelta
5
- from urllib.parse import urlparse
6
-
7
- import cachetools.func
8
- import jwt
9
- import requests
10
- from cachetools import TTLCache, cached
11
- from pydantic import BaseModel, model_validator
12
-
13
- from .exceptions import USSOException
14
- from .schemas import UserData
15
-
16
- logger = logging.getLogger("usso")
17
-
18
-
19
- def get_authorization_scheme_param(
20
- authorization_header_value: str | None,
21
- ) -> tuple[str, str]:
22
- if not authorization_header_value:
23
- return "", ""
24
- scheme, _, param = authorization_header_value.partition(" ")
25
- return scheme, param
26
-
27
-
28
- def decode_token(key, token: str, algorithms=["RS256"], **kwargs) -> dict:
29
- try:
30
- decoded = jwt.decode(token, key, algorithms=algorithms)
31
- decoded["data"] = decoded
32
- decoded["token"] = token
33
- 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
55
- 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)
59
-
60
-
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))
70
- def get_jwk_keys(jwk_url: str) -> jwt.PyJWKClient:
71
- return jwt.PyJWKClient(jwk_url, headers={"User-Agent": "usso-python"})
72
-
73
-
74
- def decode_token_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
75
- """Return the user associated with a token value."""
76
- try:
77
- jwk_client = get_jwk_keys(jwk_url)
78
- signing_key = jwk_client.get_signing_key_from_jwt(token)
79
- 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
- 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)
92
-
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
-
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
- class Usso:
131
-
132
- def __init__(
133
- self,
134
- *,
135
- jwt_config: (
136
- str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
137
- ) = None,
138
- jwk_url: str | None = None,
139
- secret: str | None = None,
140
- ):
141
- if jwt_config is None:
142
- jwt_config = os.getenv("USSO_JWT_CONFIG")
143
-
144
- if jwt_config is None:
145
- if not jwk_url:
146
- jwk_url = os.getenv("USSO_JWK_URL") or os.getenv("USSO_JWKS_URL")
147
- 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")
153
- if secret:
154
- self.jwt_configs = [JWTConfig(secret=secret)]
155
- return
156
-
157
- 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
- )
164
- )
165
-
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
172
-
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
180
-
181
- def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
182
- """Return the user associated with a token value."""
183
- exp = None
184
- for jwk_config in self.jwt_configs:
185
- try:
186
- user_data = jwk_config.decode(token)
187
- 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
-
194
- return user_data
195
-
196
- except USSOException as e:
197
- exp = e
198
-
199
- if kwargs.get("raise_exception", True):
200
- 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
- )
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)
@@ -1,39 +0,0 @@
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)
@@ -1,61 +0,0 @@
1
- import os
2
- import unittest
3
-
4
- from usso.api import UssoAPI
5
- from usso.core import UserData
6
-
7
-
8
- class TestAPI(unittest.TestCase):
9
- def get_usso(self):
10
- return UssoAPI(
11
- url="https://sso.usso.io",
12
- api_key=os.getenv("USSO_API_KEY"),
13
- )
14
-
15
- def test_get_users(self):
16
- usso_api = self.get_usso()
17
- users = usso_api.get_users()
18
- self.assertIsInstance(users, list)
19
- for user in users:
20
- self.assertIsInstance(user, UserData)
21
-
22
- def test_get_user(self):
23
- usso_api = self.get_usso()
24
- users = usso_api.get_users()
25
- if len(users) == 0:
26
- self.skipTest("No users found")
27
- user = users[0]
28
- usso_api = self.get_usso()
29
- user = usso_api.get_user(user.user_id)
30
- self.assertIsInstance(user, UserData)
31
-
32
- def test_get_user_by_credentials(self):
33
- usso_api = self.get_usso()
34
- users = usso_api._request(endpoint="website/users")
35
- if len(users) == 0:
36
- self.skipTest("No users found")
37
- for user in users:
38
- for auth in user["authenticators"]:
39
- cred = {
40
- "auth_method": auth["auth_method"],
41
- "representor": auth["representor"],
42
- }
43
- user = usso_api.get_user_by_credentials(cred)
44
- self.assertIsInstance(user, UserData)
45
-
46
- def test_create_user_by_credentials(self):
47
- import requests
48
-
49
- usso_api = self.get_usso()
50
- telegram_id = os.getenv("TELEGRAM_ID")
51
- cred = {"auth_method": "telegram", "representor": telegram_id}
52
- try:
53
- usso_api.create_user_by_credentials(credentials=cred)
54
- except requests.HTTPError as e:
55
- if e.response.status_code == 400:
56
- if e.response.json().get("error") == "already_exists":
57
- self.skipTest("Credential already exists")
58
-
59
-
60
- if __name__ == "__main__":
61
- unittest.main()
@@ -1,81 +0,0 @@
1
- import unittest
2
- import uuid
3
-
4
- from usso.core import Usso
5
- from usso.exceptions import USSOException
6
-
7
-
8
- def generate_expired_token():
9
- # Your code to generate an expired token goes here
10
- pass
11
-
12
-
13
- # Generate an invalid token for testing
14
- def generate_invalid_token():
15
- # Your code to generate an invalid token goes here
16
- pass
17
-
18
-
19
- # Generate a valid token for testing
20
- def generate_valid_token():
21
- # Your code to generate a valid token goes here
22
- pass
23
-
24
-
25
- class TestCore(unittest.TestCase):
26
- def test_user_data_from_token_valid_token(self):
27
- return
28
- # Generate a valid token for testing
29
- valid_token = generate_valid_token()
30
-
31
- # Call the user_data_from_token function with the valid token
32
- user_data = Usso().user_data_from_token(valid_token)
33
-
34
- # Assert that the user_data is not None
35
- self.assertIsNotNone(user_data)
36
-
37
- # Assert that the user_data has the expected attributes
38
- self.assertEqual(user_data.uid, uuid.UUID(""))
39
- self.assertEqual(user_data.token, valid_token)
40
- # Add more assertions for other attributes
41
- # Generate an expired token for testing
42
-
43
- def test_user_data_from_token_expired_token(self):
44
- return
45
-
46
- # Generate an expired token for testing
47
- expired_token = generate_expired_token()
48
-
49
- # Call the user_data_from_token function with the expired token
50
- user_data = Usso().user_data_from_token(expired_token)
51
-
52
- # Assert that the user_data is None
53
- self.assertIsNone(user_data)
54
-
55
- # Assert that the USSOException is raised with the expected error
56
- with self.assertRaises(USSOException) as context:
57
- Usso().user_data_from_token(expired_token, raise_exception=True)
58
- self.assertEqual(context.exception.error, "expired_signature")
59
-
60
- def test_user_data_from_token_invalid_token(self):
61
- return
62
-
63
- # Generate an invalid token for testing
64
- invalid_token = generate_invalid_token()
65
-
66
- # Call the user_data_from_token function with the invalid token
67
- user_data = Usso().user_data_from_token(invalid_token)
68
-
69
- # Assert that the user_data is None
70
- self.assertIsNone(user_data)
71
-
72
- # Assert that the USSOException is raised with the expected error
73
- with self.assertRaises(USSOException) as context:
74
- Usso().user_data_from_token(invalid_token, raise_exception=True)
75
- self.assertEqual(context.exception.error, "invalid_signature")
76
-
77
- # Add more test cases for other scenarios
78
-
79
-
80
- if __name__ == "__main__":
81
- unittest.main()
@@ -1,17 +0,0 @@
1
- # the inclusion of the tests module is not meant to offer best practices for
2
- # testing in general, but rather to support the `find_packages` example in
3
- # setup.py that excludes installing the "tests" package
4
-
5
- import unittest
6
-
7
-
8
- class TestSimple(unittest.TestCase):
9
- def test_import(self):
10
- import usso
11
-
12
- usso.Usso()
13
- usso.UserData(user_id="123")
14
-
15
-
16
- if __name__ == "__main__":
17
- unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes