usso 0.27.21__py3-none-any.whl → 0.28.0__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/api.py DELETED
@@ -1,174 +0,0 @@
1
- import logging
2
-
3
- import requests
4
- from singleton import Singleton
5
-
6
- from usso.core import UserData, Usso
7
-
8
-
9
- class UssoAPI(metaclass=Singleton):
10
- def __init__(
11
- self,
12
- url: str = "https://api.usso.io",
13
- api_key: str = None,
14
- refresh_token: str = None,
15
- ):
16
- if url and not url.startswith("http"):
17
- url = f"https://{url}"
18
- url = url.rstrip("/")
19
- self.url = url
20
- assert (
21
- api_key or refresh_token
22
- ), "Either api_key or refresh_token must be provided"
23
- self.api_key = api_key
24
- self.refresh_token = refresh_token
25
- self.access_token = None
26
-
27
- def _refresh(self):
28
- if not self.refresh_token:
29
- return
30
-
31
- url = f"{self.url}/auth/refresh"
32
-
33
- if self.refresh_token:
34
- headers = {
35
- "Authorization": f"Bearer {self.refresh_token}",
36
- "content-type": "application/json",
37
- }
38
-
39
- resp = requests.post(url, headers=headers)
40
- self.access_token = resp.json().get("access_token")
41
-
42
- def _access_valid(self) -> bool:
43
- if not self.access_token:
44
- return False
45
-
46
- user_data = Usso(
47
- jwks_url=f"{self.url}/website/jwks.json?"
48
- ).user_data_from_token(self.access_token)
49
- if user_data:
50
- return True
51
- return False
52
-
53
- def _request(
54
- self,
55
- method="get",
56
- endpoint: str = "",
57
- data: dict = None,
58
- **kwargs,
59
- ) -> dict:
60
- url = f"{self.url}/{endpoint}"
61
- headers = {"content-type": "application/json"}
62
- if self.api_key:
63
- headers["x-api-key"] = self.api_key
64
- elif self.refresh_token:
65
- if not self.access_token:
66
- self._refresh()
67
- headers["Authorization"] = f"Bearer {self.access_token}"
68
-
69
- resp = requests.request(
70
- method,
71
- url,
72
- headers=headers,
73
- json=data,
74
- )
75
- if kwargs.get("raise_exception", True):
76
- try:
77
- resp_json = resp.json()
78
- resp.raise_for_status()
79
- except requests.HTTPError as e:
80
- logging.error(f"Error: {e}")
81
- logging.error(f"Response: {resp_json}")
82
- raise e
83
- except Exception as e:
84
- logging.error(f"Error: {e}")
85
- logging.error(f"Response: {resp.text}")
86
- raise e
87
- return resp.json()
88
-
89
- def get_users(self, **kwargs) -> list[UserData]:
90
- users_dict = self._request(endpoint="website/users", **kwargs)
91
-
92
- return [UserData(user_id=user.get("uid"), **user) for user in users_dict]
93
-
94
- def get_user(self, user_id: str, **kwargs) -> UserData:
95
- user_dict = self._request(
96
- endpoint=f"website/users/{user_id}",
97
- **kwargs,
98
- )
99
- return UserData(user_id=user_dict.get("uid"), **user_dict)
100
-
101
- def get_user_by_credentials(self, credentials: dict, **kwargs) -> UserData:
102
- user_dict = self._request(
103
- endpoint="website/users/credentials",
104
- data=credentials,
105
- **kwargs,
106
- )
107
- return UserData(user_id=user_dict.get("uid"), **user_dict)
108
-
109
- def create_user(self, user_data: dict, **kwargs) -> UserData:
110
- user_dict = self._request(
111
- method="post",
112
- endpoint="website/users",
113
- data=user_data,
114
- **kwargs,
115
- )
116
-
117
- return UserData(user_id=user_dict.get("uid"), **user_dict)
118
-
119
- def create_user_credentials(
120
- self, user_id: str, credentials: dict, **kwargs
121
- ) -> UserData:
122
- user_dict = self._request(
123
- method="post",
124
- endpoint=f"website/users/{user_id}/credentials",
125
- data=credentials,
126
- **kwargs,
127
- )
128
- return UserData(user_id=user_dict.get("uid"), **user_dict)
129
-
130
- def create_user_by_credentials(
131
- self,
132
- user_data: dict | None = None,
133
- credentials: dict | None = None,
134
- **kwargs,
135
- ) -> UserData:
136
- user_data = user_data or {}
137
- if credentials:
138
- user_data["authenticators"] = [credentials]
139
- user_dict = self._request(
140
- method="post",
141
- endpoint="website/users",
142
- data=credentials,
143
- **kwargs,
144
- )
145
- return UserData(user_id=user_dict.get("uid"), **user_dict)
146
-
147
- def get_user_payload(self, user_id: str, **kwargs) -> dict:
148
- return self._request(endpoint=f"website/users/{user_id}/payload", **kwargs)
149
-
150
- def update_user_payload(
151
- self,
152
- user_id: str,
153
- payload: dict,
154
- **kwargs,
155
- ) -> dict:
156
- return self._request(
157
- method="patch",
158
- endpoint=f"website/users/{user_id}/payload",
159
- data=payload,
160
- **kwargs,
161
- )
162
-
163
- def set_user_payload(
164
- self,
165
- user_id: str,
166
- payload: dict,
167
- **kwargs,
168
- ) -> dict:
169
- return self._request(
170
- method="put",
171
- endpoint=f"website/users/{user_id}/payload",
172
- data=payload,
173
- **kwargs,
174
- )
usso/client/async_api.py DELETED
@@ -1,159 +0,0 @@
1
- import logging
2
-
3
- import httpx
4
- from singleton import Singleton
5
-
6
- from usso.core import UserData, Usso
7
-
8
-
9
- class AsyncUssoAPI(metaclass=Singleton):
10
- def __init__(
11
- self,
12
- url: str = "https://api.usso.io",
13
- api_key: str = None,
14
- refresh_token: str = None,
15
- ):
16
- if url and not url.startswith("http"):
17
- url = f"https://{url}"
18
- url = url.rstrip("/")
19
- self.url = url
20
- assert (
21
- api_key or refresh_token
22
- ), "Either api_key or refresh_token must be provided"
23
- self.api_key = api_key
24
- self.refresh_token = refresh_token
25
- self.access_token = None
26
-
27
- async def _refresh(self, **kwargs):
28
- if not self.refresh_token:
29
- return
30
-
31
- url = f"{self.url}/auth/refresh"
32
- headers = {
33
- "Authorization": f"Bearer {self.refresh_token}",
34
- "Content-Type": "application/json",
35
- }
36
-
37
- async with httpx.AsyncClient() as client:
38
- resp = await client.post(url, headers=headers)
39
- if kwargs.get("raise_exception", True):
40
- resp.raise_for_status()
41
- self.access_token = resp.json().get("access_token")
42
-
43
- def _access_valid(self) -> bool:
44
- if not self.access_token:
45
- return False
46
-
47
- user_data = Usso(
48
- jwks_url=f"{self.url}/website/jwks.json?"
49
- ).user_data_from_token(self.access_token)
50
- return bool(user_data)
51
-
52
- async def _request(
53
- self,
54
- method="get",
55
- endpoint: str = "",
56
- data: dict = None,
57
- **kwargs,
58
- ) -> dict:
59
- url = f"{self.url}/{endpoint}"
60
- headers = {"Content-Type": "application/json"}
61
- if self.api_key:
62
- headers["x-api-key"] = self.api_key
63
- elif self.refresh_token:
64
- if not self.access_token:
65
- await self._refresh()
66
- headers["Authorization"] = f"Bearer {self.access_token}"
67
-
68
- async with httpx.AsyncClient() as client:
69
- try:
70
- resp = await client.request(
71
- method,
72
- url,
73
- headers=headers,
74
- json=data,
75
- )
76
- resp.raise_for_status()
77
- return resp.json()
78
- except httpx.HTTPStatusError as e:
79
- logging.error(f"HTTP error: {e.response.status_code} {e.response.text}")
80
- raise e
81
- except Exception as e:
82
- logging.error(f"Unexpected error: {e}")
83
- raise e
84
-
85
- async def get_users(self, **kwargs) -> list[UserData]:
86
- users_dict = await self._request(endpoint="website/users", **kwargs)
87
- return [UserData(user_id=user.get("uid"), **user) for user in users_dict]
88
-
89
- async def get_user(self, user_id: str, **kwargs) -> UserData:
90
- user_dict = await self._request(endpoint=f"website/users/{user_id}", **kwargs)
91
- return UserData(user_id=user_dict.get("uid"), **user_dict)
92
-
93
- async def get_user_by_credentials(self, credentials: dict, **kwargs) -> UserData:
94
- user_dict = await self._request(
95
- endpoint="website/users/credentials", data=credentials, **kwargs
96
- )
97
- return UserData(user_id=user_dict.get("uid"), **user_dict)
98
-
99
- async def create_user(self, user_data: dict, **kwargs) -> UserData:
100
- user_dict = await self._request(
101
- method="post", endpoint="website/users", data=user_data, **kwargs
102
- )
103
- return UserData(user_id=user_dict.get("uid"), **user_dict)
104
-
105
- async def create_user_credentials(
106
- self, user_id: str, credentials: dict, **kwargs
107
- ) -> UserData:
108
- user_dict = await self._request(
109
- method="post",
110
- endpoint=f"website/users/{user_id}/credentials",
111
- data=credentials,
112
- **kwargs,
113
- )
114
- return UserData(user_id=user_dict.get("uid"), **user_dict)
115
-
116
- async def create_user_by_credentials(
117
- self,
118
- user_data: dict | None = None,
119
- credentials: dict | None = None,
120
- **kwargs,
121
- ) -> UserData:
122
- user_data = user_data or {}
123
- if credentials:
124
- user_data["authenticators"] = [credentials]
125
- user_dict = await self._request(
126
- method="post", endpoint="website/users", data=credentials, **kwargs
127
- )
128
- return UserData(user_id=user_dict.get("uid"), **user_dict)
129
-
130
- async def get_user_payload(self, user_id: str, **kwargs) -> dict:
131
- return await self._request(
132
- endpoint=f"website/users/{user_id}/payload", **kwargs
133
- )
134
-
135
- async def update_user_payload(
136
- self,
137
- user_id: str,
138
- payload: dict,
139
- **kwargs,
140
- ) -> dict:
141
- return await self._request(
142
- method="patch",
143
- endpoint=f"website/users/{user_id}/payload",
144
- data=payload,
145
- **kwargs,
146
- )
147
-
148
- async def set_user_payload(
149
- self,
150
- user_id: str,
151
- payload: dict,
152
- **kwargs,
153
- ) -> dict:
154
- return await self._request(
155
- method="put",
156
- endpoint=f"website/users/{user_id}/payload",
157
- data=payload,
158
- **kwargs,
159
- )
usso/core.py DELETED
@@ -1,160 +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 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
- try:
83
- parsed = urlparse(jwk_url)
84
- url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
85
- response = httpx.post(url, json={"api_key": api_key})
86
- response.raise_for_status()
87
- return UserData(**response.json())
88
- except Exception as e:
89
- _handle_exception("error", message=str(e))
90
-
91
-
92
-
93
- class Usso:
94
- def __init__(
95
- self,
96
- *,
97
- jwt_config: (
98
- str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
99
- ) = None,
100
- jwk_url: str | None = None,
101
- secret: str | None = None,
102
- ):
103
- self.jwt_configs = self._initialize_configs(jwt_config, jwk_url, secret)
104
-
105
- def _initialize_configs(
106
- self,
107
- jwt_config: (
108
- str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
109
- ) = None,
110
- jwk_url: str | None = None,
111
- secret: str | None = None,
112
- ):
113
- """Initialize JWT configurations."""
114
- if jwt_config is None:
115
- jwt_config = os.getenv("USSO_JWT_CONFIG")
116
-
117
- if jwt_config is None:
118
- jwk_url = jwk_url or os.getenv("USSO_JWK_URL") or os.getenv("USSO_JWKS_URL")
119
- secret = secret or os.getenv("USSO_SECRET")
120
- if jwk_url:
121
- return [JWTConfig(jwk_url=jwk_url)]
122
- if secret:
123
- return [JWTConfig(secret=secret)]
124
- raise ValueError(
125
- "Provide jwt_config, jwk_url, or secret, or set the appropriate environment variables."
126
- )
127
-
128
- if isinstance(jwt_config, (str, dict, JWTConfig)):
129
- return [self._parse_config(jwt_config)]
130
- if isinstance(jwt_config, list):
131
- return [self._parse_config(config) for config in jwt_config]
132
- raise ValueError("Invalid jwt_config format")
133
-
134
- def _parse_config(self, config):
135
- """Parse a single JWT configuration."""
136
- if isinstance(config, str):
137
- config = json.loads(config)
138
- if isinstance(config, dict):
139
- return JWTConfig(**config)
140
- return config
141
-
142
- def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
143
- """Return the user associated with a token value."""
144
- exp = None
145
- for jwk_config in self.jwt_configs:
146
- try:
147
- user_data = jwk_config.decode(token)
148
- if user_data.token_type.lower() != kwargs.get("token_type", "access"):
149
- _handle_exception("invalid_token_type", **kwargs)
150
- return user_data
151
- except USSOException as e:
152
- exp = e
153
-
154
- if kwargs.get("raise_exception", True):
155
- if exp:
156
- _handle_exception(exp.error, message=str(exp), **kwargs)
157
- _handle_exception("unauthorized", **kwargs)
158
-
159
- def user_data_from_api_key(self, api_key: str):
160
- return fetch_api_key_data(self.jwt_configs[0].jwk_url, api_key)
usso/fastapi/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- from .integration import (
2
- jwt_access_security,
3
- jwt_access_security_None,
4
- jwt_access_security_ws,
5
- )
6
-
7
- __all__ = ["jwt_access_security", "jwt_access_security_ws", "jwt_access_security_None"]
@@ -1,88 +0,0 @@
1
- import logging
2
-
3
- from fastapi import Request, WebSocket
4
- from fastapi.responses import JSONResponse
5
- from starlette.status import HTTP_401_UNAUTHORIZED
6
-
7
- from usso.exceptions import USSOException
8
-
9
- from ..core import UserData, Usso, get_authorization_scheme_param
10
-
11
- logger = logging.getLogger("usso")
12
-
13
-
14
- def get_request_token(request: Request | WebSocket) -> UserData | None:
15
- authorization = request.headers.get("Authorization")
16
- token = None
17
-
18
- if authorization:
19
- scheme, credentials = get_authorization_scheme_param(authorization)
20
- if scheme.lower() == "bearer":
21
- token = credentials
22
-
23
- else:
24
- cookie_token = request.cookies.get("usso_access_token")
25
- if cookie_token:
26
- token = cookie_token
27
-
28
- return token
29
-
30
-
31
- def jwt_access_security_None(request: Request, jwt_config=None) -> UserData | None:
32
- """Return the user associated with a token value."""
33
- api_key = request.headers.get("x-api-key")
34
- if api_key:
35
- return Usso(jwt_config=jwt_config).user_data_from_api_key(api_key)
36
-
37
- token = get_request_token(request)
38
- if not token:
39
- return None
40
- return Usso(jwt_config=jwt_config).user_data_from_token(
41
- token, raise_exception=False
42
- )
43
-
44
-
45
- def jwt_access_security(request: Request, jwt_config=None) -> UserData | None:
46
- """Return the user associated with a token value."""
47
- api_key = request.headers.get("x-api-key")
48
- if api_key:
49
- return Usso(jwt_config=jwt_config).user_data_from_api_key(api_key)
50
-
51
- token = get_request_token(request)
52
- if not token:
53
- raise USSOException(
54
- status_code=HTTP_401_UNAUTHORIZED,
55
- error="unauthorized",
56
- message="No token provided",
57
- )
58
-
59
- return Usso(jwt_config=jwt_config).user_data_from_token(token)
60
-
61
-
62
- def jwt_access_security_ws(websocket: WebSocket, jwt_config=None) -> UserData | None:
63
- """Return the user associated with a token value."""
64
- api_key = websocket.headers.get("x-api-key")
65
- if api_key:
66
- return Usso(jwt_config=jwt_config).user_data_from_api_key(api_key)
67
-
68
- token = get_request_token(websocket)
69
- if not token:
70
- raise USSOException(
71
- status_code=HTTP_401_UNAUTHORIZED,
72
- error="unauthorized",
73
- message="No token provided",
74
- )
75
-
76
- return Usso(jwt_config=jwt_config).user_data_from_token(token)
77
-
78
-
79
- async def usso_exception_handler(request: Request, exc: USSOException):
80
- return JSONResponse(
81
- status_code=exc.status_code,
82
- content={"message": exc.message, "error": exc.error},
83
- )
84
-
85
-
86
- EXCEPTION_HANDLERS = {
87
- USSOException: usso_exception_handler,
88
- }
usso/schemas.py DELETED
@@ -1,67 +0,0 @@
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])