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/__init__.py +23 -2
- usso/auth/__init__.py +9 -0
- usso/auth/api_key.py +43 -0
- usso/auth/client.py +85 -0
- usso/auth/config.py +115 -0
- usso/exceptions.py +13 -0
- usso/integrations/django/__init__.py +3 -0
- usso/{django → integrations/django}/middleware.py +37 -29
- usso/integrations/fastapi/__init__.py +3 -0
- usso/integrations/fastapi/dependency.py +95 -0
- usso/models/user.py +119 -0
- usso/session/async_session.py +16 -10
- usso/session/base_session.py +29 -47
- usso/session/session.py +10 -36
- usso/utils/method_utils.py +12 -0
- usso/utils/string_utils.py +7 -0
- usso-0.28.0.dist-info/METADATA +172 -0
- usso-0.28.0.dist-info/RECORD +24 -0
- {usso-0.27.21.dist-info → usso-0.28.0.dist-info}/WHEEL +1 -1
- usso/b64tools.py +0 -20
- usso/client/__init__.py +0 -4
- usso/client/api.py +0 -174
- usso/client/async_api.py +0 -159
- usso/core.py +0 -160
- usso/fastapi/__init__.py +0 -7
- usso/fastapi/integration.py +0 -88
- usso/schemas.py +0 -67
- usso-0.27.21.dist-info/METADATA +0 -110
- usso-0.27.21.dist-info/RECORD +0 -22
- /usso/{django → utils}/__init__.py +0 -0
- {usso-0.27.21.dist-info → usso-0.28.0.dist-info}/entry_points.txt +0 -0
- {usso-0.27.21.dist-info → usso-0.28.0.dist-info/licenses}/LICENSE.txt +0 -0
- {usso-0.27.21.dist-info → usso-0.28.0.dist-info}/top_level.txt +0 -0
usso/__init__.py
CHANGED
@@ -1,4 +1,25 @@
|
|
1
|
-
|
1
|
+
"""USSO - Universal Single Sign-On Client
|
2
|
+
|
3
|
+
A plug-and-play client for integrating universal single sign-on (SSO)
|
4
|
+
with Python frameworks, enabling secure and seamless authentication
|
5
|
+
across microservices.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .auth import APIHeaderConfig, AuthConfig, HeaderConfig, UssoAuth
|
2
9
|
from .exceptions import USSOException
|
10
|
+
from .models.user import UserData
|
11
|
+
|
12
|
+
__version__ = "0.28.0"
|
3
13
|
|
4
|
-
__all__ = [
|
14
|
+
__all__ = [
|
15
|
+
# Main client
|
16
|
+
"UssoAuth",
|
17
|
+
# Configuration
|
18
|
+
"AuthConfig",
|
19
|
+
"HeaderConfig",
|
20
|
+
"APIHeaderConfig",
|
21
|
+
# Models
|
22
|
+
"UserData",
|
23
|
+
# Exceptions
|
24
|
+
"USSOException",
|
25
|
+
]
|
usso/auth/__init__.py
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
"""USSO Authentication Module.
|
2
|
+
|
3
|
+
This module provides the core authentication functionality for USSO.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from .client import UssoAuth
|
7
|
+
from .config import APIHeaderConfig, AuthConfig, HeaderConfig
|
8
|
+
|
9
|
+
__all__ = ["UssoAuth", "AuthConfig", "HeaderConfig", "APIHeaderConfig"]
|
usso/auth/api_key.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
import logging
|
2
|
+
from urllib.parse import urlparse
|
3
|
+
|
4
|
+
import cachetools.func
|
5
|
+
import httpx
|
6
|
+
|
7
|
+
from ..exceptions import USSOException
|
8
|
+
from ..models.user import UserData
|
9
|
+
|
10
|
+
logger = logging.getLogger("usso")
|
11
|
+
|
12
|
+
|
13
|
+
def _handle_exception(error_type: str, **kwargs):
|
14
|
+
"""Handle API key related exceptions."""
|
15
|
+
if kwargs.get("raise_exception", True):
|
16
|
+
raise USSOException(
|
17
|
+
status_code=401, error=error_type, message=kwargs.get("message")
|
18
|
+
)
|
19
|
+
logger.error(kwargs.get("message") or error_type)
|
20
|
+
|
21
|
+
|
22
|
+
@cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
|
23
|
+
def fetch_api_key_data(jwk_url: str, api_key: str):
|
24
|
+
"""Fetch user data using an API key.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
jwk_url: The JWK URL to use for verification
|
28
|
+
api_key: The API key to verify
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
UserData: The user data associated with the API key
|
32
|
+
|
33
|
+
Raises:
|
34
|
+
USSOException: If the API key is invalid or verification fails
|
35
|
+
"""
|
36
|
+
try:
|
37
|
+
parsed = urlparse(jwk_url)
|
38
|
+
url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
|
39
|
+
response = httpx.post(url, json={"api_key": api_key})
|
40
|
+
response.raise_for_status()
|
41
|
+
return UserData(**response.json())
|
42
|
+
except Exception as e:
|
43
|
+
_handle_exception("error", message=str(e))
|
usso/auth/client.py
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import usso_jwt.exceptions
|
4
|
+
import usso_jwt.schemas
|
5
|
+
|
6
|
+
from ..exceptions import _handle_exception
|
7
|
+
from ..models.user import UserData
|
8
|
+
from .api_key import fetch_api_key_data
|
9
|
+
from .config import AuthConfig, AvailableJwtConfigs
|
10
|
+
|
11
|
+
logger = logging.getLogger("usso")
|
12
|
+
|
13
|
+
|
14
|
+
class UssoAuth:
|
15
|
+
"""Main authentication client for USSO.
|
16
|
+
|
17
|
+
This client handles token validation, user data retrieval,
|
18
|
+
and API key verification.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
*,
|
24
|
+
jwt_config: AvailableJwtConfigs | None = None,
|
25
|
+
):
|
26
|
+
"""Initialize the USSO authentication client.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
jwt_config: JWT configuration(s) to use for token validation
|
30
|
+
"""
|
31
|
+
if jwt_config is None:
|
32
|
+
jwt_config = AuthConfig()
|
33
|
+
self.jwt_configs = AuthConfig.validate_jwt_configs(jwt_config)
|
34
|
+
|
35
|
+
def user_data_from_token(
|
36
|
+
self,
|
37
|
+
token: str,
|
38
|
+
*,
|
39
|
+
expected_acr: str | None = "access",
|
40
|
+
raise_exception: bool = True,
|
41
|
+
**kwargs,
|
42
|
+
) -> UserData | None:
|
43
|
+
"""Get user data from a JWT token.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
token: The JWT token to validate
|
47
|
+
expected_acr: Expected authentication context reference
|
48
|
+
raise_exception: Whether to raise exception on error
|
49
|
+
**kwargs: Additional arguments to pass to token verification
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
UserData if token is valid, None otherwise
|
53
|
+
|
54
|
+
Raises:
|
55
|
+
USSOException: If token is invalid and raise_exception is True
|
56
|
+
"""
|
57
|
+
exp = None
|
58
|
+
for jwk_config in self.jwt_configs:
|
59
|
+
try:
|
60
|
+
jwt_obj = usso_jwt.schemas.JWT(
|
61
|
+
token=token, config=jwk_config, payload_class=UserData
|
62
|
+
)
|
63
|
+
if jwt_obj.verify(expected_acr=expected_acr, **kwargs):
|
64
|
+
return jwt_obj.payload
|
65
|
+
except usso_jwt.exceptions.JWTError as e:
|
66
|
+
exp = e
|
67
|
+
|
68
|
+
if raise_exception:
|
69
|
+
if exp:
|
70
|
+
_handle_exception("unauthorized", message=str(exp), **kwargs)
|
71
|
+
_handle_exception("unauthorized", **kwargs)
|
72
|
+
|
73
|
+
def user_data_from_api_key(self, api_key: str) -> UserData:
|
74
|
+
"""Get user data from an API key.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
api_key: The API key to verify
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
UserData: The user data associated with the API key
|
81
|
+
|
82
|
+
Raises:
|
83
|
+
USSOException: If the API key is invalid
|
84
|
+
"""
|
85
|
+
return fetch_api_key_data(self.jwt_configs[0].jwk_url, api_key)
|
usso/auth/config.py
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Literal, Union
|
3
|
+
|
4
|
+
import usso_jwt.config
|
5
|
+
from pydantic import BaseModel, model_validator
|
6
|
+
|
7
|
+
from ..models.user import UserData
|
8
|
+
from ..utils.string_utils import get_authorization_scheme_param
|
9
|
+
|
10
|
+
|
11
|
+
class HeaderConfig(BaseModel):
|
12
|
+
type: Literal["Authorization", "Cookie", "CustomHeader"] = "Cookie"
|
13
|
+
name: str = "usso_access_token"
|
14
|
+
|
15
|
+
@model_validator(mode="before")
|
16
|
+
def validate_header(cls, data: dict):
|
17
|
+
if data.get("type") == "Authorization" and not data.get("name"):
|
18
|
+
data["name"] = "Bearer"
|
19
|
+
elif data.get("type") == "Cookie":
|
20
|
+
data["name"] = data.get("name", "usso_access_token")
|
21
|
+
elif data.get("type") == "CustomHeader":
|
22
|
+
data["name"] = data.get("name", "x-usso-access-token")
|
23
|
+
return data
|
24
|
+
|
25
|
+
def __hash__(self):
|
26
|
+
return hash(self.model_dump_json())
|
27
|
+
|
28
|
+
def get_key(self, request) -> str | None:
|
29
|
+
headers: dict[str, Any] = getattr(request, "headers", {})
|
30
|
+
cookies: dict[str, str] = getattr(
|
31
|
+
request, "cookies", headers.get("Cookie", {})
|
32
|
+
)
|
33
|
+
if self.type == "CustomHeader":
|
34
|
+
return headers.get(self.name)
|
35
|
+
elif self.type == "Cookie":
|
36
|
+
return cookies.get(self.name)
|
37
|
+
elif self.type == "Authorization":
|
38
|
+
authorization = headers.get("Authorization")
|
39
|
+
if self.type == "Authorization":
|
40
|
+
authorization = headers.get("Authorization")
|
41
|
+
scheme, credentials = get_authorization_scheme_param(authorization)
|
42
|
+
if scheme.lower() == self.name.lower():
|
43
|
+
return credentials
|
44
|
+
|
45
|
+
|
46
|
+
class APIHeaderConfig(HeaderConfig):
|
47
|
+
verify_endpoint: str = "https://sso.usso.io/api_key/verify"
|
48
|
+
|
49
|
+
|
50
|
+
class AuthConfig(usso_jwt.config.JWTConfig):
|
51
|
+
"""Configuration for JWT processing."""
|
52
|
+
|
53
|
+
api_key_header: APIHeaderConfig | None = APIHeaderConfig(
|
54
|
+
type="CustomHeader", name="x-api-key"
|
55
|
+
)
|
56
|
+
jwt_header: HeaderConfig | None = HeaderConfig()
|
57
|
+
static_api_keys: list[str] | None = None
|
58
|
+
|
59
|
+
def get_api_key(self, request) -> str | None:
|
60
|
+
if self.api_key_header:
|
61
|
+
return self.api_key_header.get_key(request)
|
62
|
+
return None
|
63
|
+
|
64
|
+
def get_jwt(self, request) -> str | None:
|
65
|
+
if self.jwt_header:
|
66
|
+
return self.jwt_header.get_key(request)
|
67
|
+
return None
|
68
|
+
|
69
|
+
def verify_token(
|
70
|
+
self, token: str, *, raise_exception: bool = True, **kwargs
|
71
|
+
) -> bool:
|
72
|
+
from usso_jwt import exceptions as jwt_exceptions
|
73
|
+
from usso_jwt import schemas
|
74
|
+
|
75
|
+
try:
|
76
|
+
return schemas.JWT(
|
77
|
+
token=token,
|
78
|
+
config=self,
|
79
|
+
payload_class=UserData,
|
80
|
+
).verify(**kwargs)
|
81
|
+
except jwt_exceptions.JWTError as e:
|
82
|
+
if raise_exception:
|
83
|
+
raise e
|
84
|
+
return False
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
def _parse_config(
|
88
|
+
cls, config: Union[str, dict, "AuthConfig"]
|
89
|
+
) -> "AuthConfig":
|
90
|
+
"""Parse a single JWT configuration."""
|
91
|
+
if isinstance(config, str):
|
92
|
+
config = json.loads(config)
|
93
|
+
if isinstance(config, dict):
|
94
|
+
return cls(**config)
|
95
|
+
if isinstance(config, cls):
|
96
|
+
return config
|
97
|
+
raise ValueError("Invalid JWT configuration")
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def validate_jwt_configs(
|
101
|
+
cls,
|
102
|
+
jwt_config: Union[
|
103
|
+
str, dict, "AuthConfig", list[str], list[dict], list["AuthConfig"]
|
104
|
+
],
|
105
|
+
) -> list["AuthConfig"]:
|
106
|
+
if isinstance(jwt_config, (str, dict, cls)):
|
107
|
+
return [cls._parse_config(jwt_config)]
|
108
|
+
if isinstance(jwt_config, list):
|
109
|
+
return [cls._parse_config(config) for config in jwt_config]
|
110
|
+
raise ValueError("Invalid jwt_config format")
|
111
|
+
|
112
|
+
|
113
|
+
AvailableJwtConfigs = (
|
114
|
+
str | dict | AuthConfig | list[str] | list[dict] | list[AuthConfig]
|
115
|
+
)
|
usso/exceptions.py
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
logger = logging.getLogger("usso")
|
4
|
+
|
1
5
|
error_messages = {
|
2
6
|
"invalid_signature": "Unauthorized. The JWT signature is invalid.",
|
3
7
|
"invalid_token": "Unauthorized. The JWT is invalid or not provided.",
|
@@ -15,3 +19,12 @@ class USSOException(Exception):
|
|
15
19
|
if message is None:
|
16
20
|
self.message = error_messages[error]
|
17
21
|
super().__init__(message)
|
22
|
+
|
23
|
+
|
24
|
+
def _handle_exception(error_type: str, **kwargs):
|
25
|
+
"""Handle JWT-related exceptions."""
|
26
|
+
if kwargs.get("raise_exception", True):
|
27
|
+
raise USSOException(
|
28
|
+
status_code=401, error=error_type, message=kwargs.get("message")
|
29
|
+
)
|
30
|
+
logger.error(kwargs.get("message") or error_type)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
from urllib.parse import urlparse
|
2
3
|
|
3
4
|
from django.conf import settings
|
4
5
|
from django.contrib.auth.models import User
|
@@ -6,17 +7,20 @@ from django.db.utils import IntegrityError
|
|
6
7
|
from django.http import JsonResponse
|
7
8
|
from django.http.request import HttpRequest
|
8
9
|
from django.utils.deprecation import MiddlewareMixin
|
9
|
-
|
10
|
-
from usso import UserData, Usso, USSOException
|
10
|
+
from usso import AuthConfig, UserData, UssoAuth, USSOException
|
11
11
|
|
12
12
|
logger = logging.getLogger("usso")
|
13
13
|
|
14
14
|
|
15
15
|
class USSOAuthenticationMiddleware(MiddlewareMixin):
|
16
|
+
@property
|
17
|
+
def jwt_config(self) -> AuthConfig:
|
18
|
+
return settings.USSO_JWT_CONFIG
|
16
19
|
|
17
20
|
def process_request(self, request: HttpRequest):
|
18
21
|
"""
|
19
|
-
Middleware to authenticate users by JWT token and create or
|
22
|
+
Middleware to authenticate users by JWT token and create or
|
23
|
+
return a user in the database.
|
20
24
|
"""
|
21
25
|
try:
|
22
26
|
if hasattr(request, "user") and request.user.is_authenticated:
|
@@ -31,44 +35,48 @@ class USSOAuthenticationMiddleware(MiddlewareMixin):
|
|
31
35
|
# Handle any errors raised by USSO authentication
|
32
36
|
return JsonResponse({"error": str(e)}, status=401)
|
33
37
|
|
34
|
-
def
|
35
|
-
|
36
|
-
if authorization:
|
37
|
-
scheme, credentials = Usso(
|
38
|
-
jwks_url=settings.USSO_JWK_URL
|
39
|
-
).get_authorization_scheme_param(authorization)
|
40
|
-
if scheme.lower() == "bearer":
|
41
|
-
return credentials # Bearer token
|
42
|
-
|
43
|
-
return request.COOKIES.get("usso_access_token")
|
38
|
+
def get_request_jwt(self, request: HttpRequest) -> str | None:
|
39
|
+
return self.jwt_config.get_jwt(request)
|
44
40
|
|
45
|
-
def jwt_access_security_none(
|
41
|
+
def jwt_access_security_none(
|
42
|
+
self,
|
43
|
+
request: HttpRequest,
|
44
|
+
) -> UserData | None:
|
46
45
|
"""Return the user associated with a token value."""
|
47
|
-
|
46
|
+
usso_auth = UssoAuth(jwt_config=self.jwt_config)
|
47
|
+
api_key = self.jwt_config.get_api_key(request)
|
48
|
+
if api_key:
|
49
|
+
return usso_auth.user_data_from_api_key(api_key)
|
50
|
+
|
51
|
+
token = self.get_request_jwt(request)
|
48
52
|
if not token:
|
49
53
|
return None
|
50
|
-
return
|
51
|
-
token, raise_exception=False
|
52
|
-
)
|
54
|
+
return usso_auth.user_data_from_token(token, raise_exception=False)
|
53
55
|
|
54
56
|
def jwt_access_security(self, request: HttpRequest) -> UserData | None:
|
55
57
|
"""Return the user associated with a token value."""
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
error="unauthorized",
|
61
|
-
)
|
58
|
+
usso_auth = UssoAuth(jwt_config=self.jwt_config)
|
59
|
+
api_key = self.jwt_config.get_api_key(request)
|
60
|
+
if api_key:
|
61
|
+
return usso_auth.user_data_from_api_key(api_key)
|
62
62
|
|
63
|
-
|
64
|
-
|
63
|
+
token = self.get_request_jwt(request)
|
64
|
+
if not token:
|
65
|
+
return None
|
66
|
+
return usso_auth.user_data_from_token(token, raise_exception=False)
|
65
67
|
|
66
68
|
def get_or_create_user(self, user_data: UserData) -> User:
|
67
69
|
"""
|
68
|
-
Check if a user exists by phone. If not, create a new user
|
70
|
+
Check if a user exists by phone. If not, create a new user
|
71
|
+
and return it.
|
69
72
|
"""
|
73
|
+
if self.jwt_config.jwk_url:
|
74
|
+
domain = urlparse(self.jwt_config.jwk_url).netloc
|
75
|
+
else:
|
76
|
+
domain = "example.com"
|
70
77
|
phone = user_data.phone
|
71
|
-
email = user_data.email or f"{user_data.user_id}@
|
78
|
+
email = user_data.email or f"{user_data.user_id}@{domain}"
|
79
|
+
# Fallback email
|
72
80
|
|
73
81
|
try:
|
74
82
|
# Try to get the user by phone
|
@@ -87,4 +95,4 @@ class USSOAuthenticationMiddleware(MiddlewareMixin):
|
|
87
95
|
|
88
96
|
except IntegrityError as e:
|
89
97
|
logger.error(f"Integrity error while creating user: {str(e)}")
|
90
|
-
raise ValueError(f"Error while creating user: {str(e)}")
|
98
|
+
raise ValueError(f"Error while creating user: {str(e)}") from e
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from starlette.status import HTTP_401_UNAUTHORIZED
|
4
|
+
|
5
|
+
from fastapi import Request, WebSocket
|
6
|
+
from fastapi.responses import JSONResponse
|
7
|
+
|
8
|
+
from ...auth import UssoAuth
|
9
|
+
from ...auth.config import AuthConfig, AvailableJwtConfigs
|
10
|
+
from ...exceptions import USSOException
|
11
|
+
from ...models.user import UserData
|
12
|
+
from ...utils.method_utils import instance_method
|
13
|
+
|
14
|
+
logger = logging.getLogger("usso")
|
15
|
+
|
16
|
+
|
17
|
+
class USSOAuthentication(UssoAuth):
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
jwt_config: AvailableJwtConfigs | None = None,
|
21
|
+
raise_exception: bool = True,
|
22
|
+
):
|
23
|
+
if jwt_config is None:
|
24
|
+
jwt_config = AuthConfig()
|
25
|
+
|
26
|
+
super().__init__(jwt_config=jwt_config)
|
27
|
+
self.raise_exception = raise_exception
|
28
|
+
|
29
|
+
@instance_method
|
30
|
+
def get_request_jwt(self, request: Request | WebSocket) -> str | None:
|
31
|
+
for jwt_config in self.jwt_configs:
|
32
|
+
token = jwt_config.get_jwt(request)
|
33
|
+
if token:
|
34
|
+
return token
|
35
|
+
return None
|
36
|
+
|
37
|
+
@instance_method
|
38
|
+
def get_request_api_key(self, request: Request | WebSocket) -> str | None:
|
39
|
+
for jwt_config in self.jwt_configs:
|
40
|
+
token = jwt_config.get_api_key(request)
|
41
|
+
if token:
|
42
|
+
return token
|
43
|
+
return None
|
44
|
+
|
45
|
+
@instance_method
|
46
|
+
def usso_access_security(self, request: Request) -> UserData | None:
|
47
|
+
"""Return the user associated with a token value."""
|
48
|
+
api_key = self.get_request_api_key(request)
|
49
|
+
if api_key:
|
50
|
+
return self.user_data_from_api_key(api_key)
|
51
|
+
|
52
|
+
token = self.get_request_jwt(request)
|
53
|
+
if token:
|
54
|
+
return self.user_data_from_token(
|
55
|
+
token, raise_exception=self.raise_exception
|
56
|
+
)
|
57
|
+
if self.raise_exception:
|
58
|
+
raise USSOException(
|
59
|
+
status_code=HTTP_401_UNAUTHORIZED,
|
60
|
+
error="unauthorized",
|
61
|
+
message="No token provided",
|
62
|
+
)
|
63
|
+
return None
|
64
|
+
|
65
|
+
@instance_method
|
66
|
+
def jwt_access_security_ws(self, websocket: WebSocket) -> UserData | None:
|
67
|
+
"""Return the user associated with a token value."""
|
68
|
+
api_key = self.get_request_api_key(websocket)
|
69
|
+
if api_key:
|
70
|
+
return self.user_data_from_api_key(api_key)
|
71
|
+
|
72
|
+
token = self.get_request_jwt(websocket)
|
73
|
+
if token:
|
74
|
+
return self.user_data_from_token(
|
75
|
+
token, raise_exception=self.raise_exception
|
76
|
+
)
|
77
|
+
if self.raise_exception:
|
78
|
+
raise USSOException(
|
79
|
+
status_code=HTTP_401_UNAUTHORIZED,
|
80
|
+
error="unauthorized",
|
81
|
+
message="No token provided",
|
82
|
+
)
|
83
|
+
return None
|
84
|
+
|
85
|
+
|
86
|
+
async def usso_exception_handler(request: Request, exc: USSOException):
|
87
|
+
return JSONResponse(
|
88
|
+
status_code=exc.status_code,
|
89
|
+
content={"message": exc.message, "error": exc.error},
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
EXCEPTION_HANDLERS = {
|
94
|
+
USSOException: usso_exception_handler,
|
95
|
+
}
|
usso/models/user.py
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from enum import StrEnum
|
3
|
+
from typing import Any, Literal
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
|
8
|
+
class TokenType(StrEnum):
|
9
|
+
ACCESS = "access"
|
10
|
+
REFRESH = "refresh"
|
11
|
+
SECURE_TOKEN = "secure"
|
12
|
+
ONE_TIME_TOKEN = "one_time"
|
13
|
+
TEMPORARY_TOKEN = "temporary"
|
14
|
+
MFA_TEMPORARY_TOKEN = "mfa_temporary"
|
15
|
+
|
16
|
+
|
17
|
+
class UserData(BaseModel):
|
18
|
+
jti: str | None = None
|
19
|
+
typ: TokenType | None = None
|
20
|
+
iss: str | None = None
|
21
|
+
aud: str | None = None
|
22
|
+
iat: int | None = None
|
23
|
+
nbf: int | None = None
|
24
|
+
exp: int | None = None
|
25
|
+
sub: str | None = None
|
26
|
+
tenant_id: str | None = None
|
27
|
+
workspace_id: str | None = None
|
28
|
+
roles: list[str] | None = None
|
29
|
+
scopes: list[str] | None = None
|
30
|
+
acr: str | None = None
|
31
|
+
signing_level: str | None = None
|
32
|
+
|
33
|
+
claims: dict | None = None
|
34
|
+
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
*,
|
38
|
+
jti: str | None = None,
|
39
|
+
typ: TokenType | None = None,
|
40
|
+
iss: str | None = None,
|
41
|
+
aud: str | None = None,
|
42
|
+
iat: int | None = None,
|
43
|
+
nbf: int | None = None,
|
44
|
+
exp: int | None = None,
|
45
|
+
sub: str | None = None,
|
46
|
+
tenant_id: str | None = None,
|
47
|
+
workspace_id: str | None = None,
|
48
|
+
roles: list[str] | None = None,
|
49
|
+
scopes: list[str] | None = None,
|
50
|
+
acr: str | None = None,
|
51
|
+
signing_level: str | None = None,
|
52
|
+
**kwargs,
|
53
|
+
):
|
54
|
+
super().__init__(
|
55
|
+
jti=jti,
|
56
|
+
typ=typ,
|
57
|
+
iss=iss,
|
58
|
+
aud=aud,
|
59
|
+
iat=iat,
|
60
|
+
nbf=nbf,
|
61
|
+
exp=exp,
|
62
|
+
sub=sub,
|
63
|
+
tenant_id=tenant_id,
|
64
|
+
workspace_id=workspace_id,
|
65
|
+
roles=roles,
|
66
|
+
scopes=scopes,
|
67
|
+
acr=acr,
|
68
|
+
signing_level=signing_level,
|
69
|
+
)
|
70
|
+
self.claims = self.model_dump() | kwargs
|
71
|
+
|
72
|
+
@property
|
73
|
+
def user_id(self) -> str:
|
74
|
+
if self.claims and "user_id" in self.claims:
|
75
|
+
return self.claims["user_id"]
|
76
|
+
return self.sub or ""
|
77
|
+
|
78
|
+
@property
|
79
|
+
def email(self) -> str:
|
80
|
+
if self.claims and "email" in self.claims:
|
81
|
+
return self.claims["email"]
|
82
|
+
return ""
|
83
|
+
|
84
|
+
@property
|
85
|
+
def phone(self) -> str:
|
86
|
+
if self.claims and "phone" in self.claims:
|
87
|
+
return self.claims["phone"]
|
88
|
+
return ""
|
89
|
+
|
90
|
+
def model_dump(
|
91
|
+
self,
|
92
|
+
*,
|
93
|
+
mode: Literal["json", "python"] | str = "python",
|
94
|
+
include=None,
|
95
|
+
exclude=None,
|
96
|
+
context: Any | None = None,
|
97
|
+
by_alias: bool | None = None,
|
98
|
+
exclude_unset: bool = False,
|
99
|
+
exclude_defaults: bool = False,
|
100
|
+
exclude_none: bool = True,
|
101
|
+
round_trip: bool = False,
|
102
|
+
warnings: bool | Literal["none", "warn", "error"] = True,
|
103
|
+
fallback: Callable[[Any], Any] | None = None,
|
104
|
+
serialize_as_any: bool = False,
|
105
|
+
):
|
106
|
+
return super().model_dump(
|
107
|
+
mode=mode,
|
108
|
+
include=include,
|
109
|
+
exclude=exclude,
|
110
|
+
context=context,
|
111
|
+
by_alias=by_alias,
|
112
|
+
exclude_unset=exclude_unset,
|
113
|
+
exclude_defaults=exclude_defaults,
|
114
|
+
exclude_none=exclude_none,
|
115
|
+
round_trip=round_trip,
|
116
|
+
warnings=warnings,
|
117
|
+
fallback=fallback,
|
118
|
+
serialize_as_any=serialize_as_any,
|
119
|
+
)
|