workos 1.5.1__py3-none-any.whl → 5.38.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.
- workos/__about__.py +1 -1
- workos/__init__.py +3 -7
- workos/_base_client.py +138 -0
- workos/_client_configuration.py +10 -0
- workos/api_keys.py +53 -0
- workos/async_client.py +144 -0
- workos/audit_logs.py +125 -0
- workos/client.py +110 -18
- workos/directory_sync.py +379 -99
- workos/events.py +111 -0
- workos/exceptions.py +53 -26
- workos/fga.py +649 -0
- workos/mfa.py +205 -0
- workos/organization_domains.py +179 -0
- workos/organizations.py +403 -73
- workos/passwordless.py +67 -43
- workos/pipes.py +93 -0
- workos/portal.py +51 -28
- workos/session.py +337 -0
- workos/sso.py +311 -101
- workos/types/__init__.py +4 -0
- workos/types/api_keys/__init__.py +1 -0
- workos/types/api_keys/api_keys.py +20 -0
- workos/types/audit_logs/__init__.py +6 -0
- workos/types/audit_logs/audit_log_event.py +16 -0
- workos/types/audit_logs/audit_log_event_actor.py +12 -0
- workos/types/audit_logs/audit_log_event_context.py +8 -0
- workos/types/audit_logs/audit_log_event_target.py +12 -0
- workos/types/audit_logs/audit_log_export.py +18 -0
- workos/types/audit_logs/audit_log_metadata.py +4 -0
- workos/types/directory_sync/__init__.py +5 -0
- workos/types/directory_sync/directory.py +31 -0
- workos/types/directory_sync/directory_group.py +16 -0
- workos/types/directory_sync/directory_state.py +28 -0
- workos/types/directory_sync/directory_type.py +24 -0
- workos/types/directory_sync/directory_user.py +50 -0
- workos/types/directory_sync/list_filters.py +21 -0
- workos/types/events/__init__.py +13 -0
- workos/types/events/authentication_payload.py +70 -0
- workos/types/events/connection_payload_with_legacy_fields.py +5 -0
- workos/types/events/directory_group_membership_payload.py +9 -0
- workos/types/events/directory_group_with_previous_attributes.py +6 -0
- workos/types/events/directory_payload.py +16 -0
- workos/types/events/directory_payload_with_legacy_fields.py +29 -0
- workos/types/events/directory_user_with_previous_attributes.py +6 -0
- workos/types/events/event.py +324 -0
- workos/types/events/event_model.py +103 -0
- workos/types/events/event_type.py +59 -0
- workos/types/events/list_filters.py +10 -0
- workos/types/events/organization_domain_verification_failed_payload.py +14 -0
- workos/types/events/previous_attributes.py +3 -0
- workos/types/events/session_payload.py +27 -0
- workos/types/feature_flags/__init__.py +3 -0
- workos/types/feature_flags/feature_flag.py +12 -0
- workos/types/feature_flags/list_filters.py +5 -0
- workos/types/fga/__init__.py +5 -0
- workos/types/fga/authorization_resource_types.py +9 -0
- workos/types/fga/authorization_resources.py +10 -0
- workos/types/fga/check.py +51 -0
- workos/types/fga/list_filters.py +24 -0
- workos/types/fga/warnings.py +33 -0
- workos/types/fga/warrant.py +49 -0
- workos/types/list_resource.py +198 -0
- workos/types/metadata.py +4 -0
- workos/types/mfa/__init__.py +5 -0
- workos/types/mfa/authentication_challenge.py +14 -0
- workos/types/mfa/authentication_challenge_verification_response.py +9 -0
- workos/types/mfa/authentication_factor.py +70 -0
- workos/types/mfa/authentication_factor_totp_and_challenge_response.py +10 -0
- workos/types/mfa/enroll_authentication_factor_type.py +8 -0
- workos/types/organization_domains/__init__.py +1 -0
- workos/types/organization_domains/organization_domain.py +18 -0
- workos/types/organizations/__init__.py +6 -0
- workos/types/organizations/domain_data_input.py +7 -0
- workos/types/organizations/list_filters.py +6 -0
- workos/types/organizations/organization.py +13 -0
- workos/types/organizations/organization_common.py +12 -0
- workos/types/passwordless/__init__.py +2 -0
- workos/types/passwordless/passwordless_session.py +12 -0
- workos/types/passwordless/passwordless_session_type.py +3 -0
- workos/types/pipes/__init__.py +6 -0
- workos/types/pipes/pipes.py +34 -0
- workos/types/portal/__init__.py +2 -0
- workos/types/portal/portal_link.py +7 -0
- workos/types/portal/portal_link_intent.py +11 -0
- workos/types/portal/portal_link_intent_options.py +9 -0
- workos/types/roles/__init__.py +0 -0
- workos/types/roles/role.py +27 -0
- workos/types/sso/__init__.py +4 -0
- workos/types/sso/connection.py +70 -0
- workos/types/sso/connection_domain.py +8 -0
- workos/types/sso/profile.py +35 -0
- workos/types/sso/sso_provider_type.py +10 -0
- workos/types/user_management/__init__.py +12 -0
- workos/types/user_management/authenticate_with_common.py +66 -0
- workos/types/user_management/authentication_response.py +53 -0
- workos/types/user_management/email_verification.py +18 -0
- workos/types/user_management/impersonator.py +8 -0
- workos/types/user_management/invitation.py +26 -0
- workos/types/user_management/list_filters.py +29 -0
- workos/types/user_management/magic_auth.py +18 -0
- workos/types/user_management/oauth_tokens.py +21 -0
- workos/types/user_management/organization_membership.py +25 -0
- workos/types/user_management/password_hash_type.py +4 -0
- workos/types/user_management/password_reset.py +18 -0
- workos/types/user_management/screen_hint.py +3 -0
- workos/types/user_management/session.py +79 -0
- workos/types/user_management/user.py +22 -0
- workos/types/user_management/user_management_provider_type.py +11 -0
- workos/types/vault/__init__.py +2 -0
- workos/types/vault/key.py +25 -0
- workos/types/vault/object.py +38 -0
- workos/types/webhooks/__init__.py +0 -0
- workos/types/webhooks/webhook.py +330 -0
- workos/types/webhooks/webhook_model.py +14 -0
- workos/types/webhooks/webhook_payload.py +4 -0
- workos/types/widgets/__init__.py +2 -0
- workos/types/widgets/widget_scope.py +4 -0
- workos/types/widgets/widget_token_response.py +7 -0
- workos/types/workos_model.py +26 -0
- workos/typing/__init__.py +1 -0
- workos/typing/literals.py +32 -0
- workos/typing/sync_or_async.py +5 -0
- workos/typing/untyped_literal.py +37 -0
- workos/typing/webhooks.py +18 -0
- workos/user_management.py +2400 -0
- workos/utils/_base_http_client.py +252 -0
- workos/utils/crypto_provider.py +39 -0
- workos/utils/http_client.py +214 -0
- workos/utils/pagination_order.py +4 -0
- workos/utils/request_helper.py +27 -0
- workos/vault.py +544 -0
- workos/webhooks.py +96 -39
- workos/widgets.py +55 -0
- {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/LICENSE +1 -1
- workos-5.38.0.dist-info/METADATA +107 -0
- workos-5.38.0.dist-info/RECORD +141 -0
- {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/WHEEL +1 -1
- workos/audit_trail.py +0 -172
- workos/resources/base.py +0 -36
- workos/resources/event.py +0 -42
- workos/resources/event_action.py +0 -11
- workos/resources/sso.py +0 -53
- workos/utils/connection_types.py +0 -17
- workos/utils/request.py +0 -95
- workos/utils/validation.py +0 -45
- workos-1.5.1.dist-info/METADATA +0 -77
- workos-1.5.1.dist-info/RECORD +0 -25
- /workos/{resources/__init__.py → py.typed} +0 -0
- {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/top_level.txt +0 -0
workos/pipes.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Dict, Optional, Protocol
|
|
2
|
+
|
|
3
|
+
from workos.types.pipes import (
|
|
4
|
+
GetAccessTokenFailureResponse,
|
|
5
|
+
GetAccessTokenResponse,
|
|
6
|
+
GetAccessTokenSuccessResponse,
|
|
7
|
+
)
|
|
8
|
+
from workos.typing.sync_or_async import SyncOrAsync
|
|
9
|
+
from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient
|
|
10
|
+
from workos.utils.request_helper import REQUEST_METHOD_POST
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PipesModule(Protocol):
|
|
14
|
+
"""Protocol defining the Pipes module interface."""
|
|
15
|
+
|
|
16
|
+
def get_access_token(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
provider: str,
|
|
20
|
+
user_id: str,
|
|
21
|
+
organization_id: Optional[str] = None,
|
|
22
|
+
) -> SyncOrAsync[GetAccessTokenResponse]:
|
|
23
|
+
"""Retrieve an access token for a third-party provider.
|
|
24
|
+
|
|
25
|
+
Kwargs:
|
|
26
|
+
provider (str): The third-party provider identifier
|
|
27
|
+
user_id (str): The WorkOS user ID
|
|
28
|
+
organization_id (str, optional): The WorkOS organization ID
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
GetAccessTokenResponse: Success response with token or failure response with error
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Pipes(PipesModule):
|
|
37
|
+
"""Sync implementation of the Pipes module."""
|
|
38
|
+
|
|
39
|
+
_http_client: SyncHTTPClient
|
|
40
|
+
|
|
41
|
+
def __init__(self, http_client: SyncHTTPClient):
|
|
42
|
+
self._http_client = http_client
|
|
43
|
+
|
|
44
|
+
def get_access_token(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
provider: str,
|
|
48
|
+
user_id: str,
|
|
49
|
+
organization_id: Optional[str] = None,
|
|
50
|
+
) -> GetAccessTokenResponse:
|
|
51
|
+
json_data: Dict[str, str] = {"user_id": user_id}
|
|
52
|
+
if organization_id is not None:
|
|
53
|
+
json_data["organization_id"] = organization_id
|
|
54
|
+
|
|
55
|
+
response = self._http_client.request(
|
|
56
|
+
f"data-integrations/{provider}/token",
|
|
57
|
+
method=REQUEST_METHOD_POST,
|
|
58
|
+
json=json_data,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response.get("active") is True:
|
|
62
|
+
return GetAccessTokenSuccessResponse.model_validate(response)
|
|
63
|
+
return GetAccessTokenFailureResponse.model_validate(response)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsyncPipes(PipesModule):
|
|
67
|
+
"""Async implementation of the Pipes module."""
|
|
68
|
+
|
|
69
|
+
_http_client: AsyncHTTPClient
|
|
70
|
+
|
|
71
|
+
def __init__(self, http_client: AsyncHTTPClient):
|
|
72
|
+
self._http_client = http_client
|
|
73
|
+
|
|
74
|
+
async def get_access_token(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
provider: str,
|
|
78
|
+
user_id: str,
|
|
79
|
+
organization_id: Optional[str] = None,
|
|
80
|
+
) -> GetAccessTokenResponse:
|
|
81
|
+
json_data: Dict[str, str] = {"user_id": user_id}
|
|
82
|
+
if organization_id is not None:
|
|
83
|
+
json_data["organization_id"] = organization_id
|
|
84
|
+
|
|
85
|
+
response = await self._http_client.request(
|
|
86
|
+
f"data-integrations/{provider}/token",
|
|
87
|
+
method=REQUEST_METHOD_POST,
|
|
88
|
+
json=json_data,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if response.get("active") is True:
|
|
92
|
+
return GetAccessTokenSuccessResponse.model_validate(response)
|
|
93
|
+
return GetAccessTokenFailureResponse.model_validate(response)
|
workos/portal.py
CHANGED
|
@@ -1,43 +1,66 @@
|
|
|
1
|
-
import
|
|
2
|
-
from workos.
|
|
3
|
-
from workos.
|
|
1
|
+
from typing import Optional, Protocol, Dict, Literal, Union
|
|
2
|
+
from workos.types.portal.portal_link import PortalLink
|
|
3
|
+
from workos.types.portal.portal_link_intent import PortalLinkIntent
|
|
4
|
+
from workos.types.portal.portal_link_intent_options import IntentOptions
|
|
5
|
+
from workos.utils.http_client import SyncHTTPClient
|
|
6
|
+
from workos.utils.request_helper import REQUEST_METHOD_POST
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
PORTAL_GENERATE_PATH = "portal/generate_link"
|
|
7
10
|
|
|
8
11
|
|
|
9
|
-
class
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def generate_link(self, intent, organization, return_url=None):
|
|
12
|
+
class PortalModule(Protocol):
|
|
13
|
+
def generate_link(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
intent: PortalLinkIntent,
|
|
17
|
+
organization_id: str,
|
|
18
|
+
return_url: Optional[str] = None,
|
|
19
|
+
success_url: Optional[str] = None,
|
|
20
|
+
intent_options: Optional[IntentOptions] = None,
|
|
21
|
+
) -> PortalLink:
|
|
21
22
|
"""Generate a link to grant access to an organization's Admin Portal
|
|
22
23
|
|
|
23
|
-
Args:
|
|
24
|
-
intent (str): The access scope for the generated Admin Portal link. Valid values are: ["sso", "dsync"]
|
|
25
|
-
organization (string): The ID of the organization the Admin Portal link will be generated for
|
|
26
|
-
|
|
27
24
|
Kwargs:
|
|
28
|
-
|
|
25
|
+
intent (PortalLinkIntent): The access scope for the generated Admin Portal link.
|
|
26
|
+
organization_id (str): The ID of the organization the Admin Portal link will be generated for.
|
|
27
|
+
return_url (str): The URL that the end user will be redirected to upon exiting the generated Admin Portal.
|
|
28
|
+
If none is provided, the default redirect link set in your WorkOS Dashboard will be used. (Optional)
|
|
29
|
+
success_url (str): The URL to which WorkOS will redirect users to upon successfully viewing Audit Logs,
|
|
30
|
+
setting up Log Streams, Single Sign On or Directory Sync. (Optional)
|
|
29
31
|
|
|
30
32
|
Returns:
|
|
31
|
-
|
|
33
|
+
PortalLink: PortalLink object with URL to redirect a User to to access an Admin Portal session.
|
|
32
34
|
"""
|
|
33
|
-
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Portal(PortalModule):
|
|
39
|
+
|
|
40
|
+
_http_client: SyncHTTPClient
|
|
41
|
+
|
|
42
|
+
def __init__(self, http_client: SyncHTTPClient):
|
|
43
|
+
self._http_client = http_client
|
|
44
|
+
|
|
45
|
+
def generate_link(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
intent: PortalLinkIntent,
|
|
49
|
+
organization_id: str,
|
|
50
|
+
return_url: Optional[str] = None,
|
|
51
|
+
success_url: Optional[str] = None,
|
|
52
|
+
intent_options: Optional[IntentOptions] = None,
|
|
53
|
+
) -> PortalLink:
|
|
54
|
+
json = {
|
|
34
55
|
"intent": intent,
|
|
35
|
-
"organization":
|
|
56
|
+
"organization": organization_id,
|
|
36
57
|
"return_url": return_url,
|
|
58
|
+
"success_url": success_url,
|
|
59
|
+
"intent_options": intent_options,
|
|
37
60
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
method=REQUEST_METHOD_POST,
|
|
41
|
-
params=params,
|
|
42
|
-
token=workos.api_key,
|
|
61
|
+
|
|
62
|
+
response = self._http_client.request(
|
|
63
|
+
PORTAL_GENERATE_PATH, method=REQUEST_METHOD_POST, json=json
|
|
43
64
|
)
|
|
65
|
+
|
|
66
|
+
return PortalLink.model_validate(response)
|
workos/session.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, List, Protocol
|
|
3
|
+
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Optional, Union, cast
|
|
7
|
+
|
|
8
|
+
import jwt
|
|
9
|
+
from jwt import PyJWKClient
|
|
10
|
+
from cryptography.fernet import Fernet
|
|
11
|
+
|
|
12
|
+
from workos.types.user_management.session import (
|
|
13
|
+
AuthenticateWithSessionCookieFailureReason,
|
|
14
|
+
AuthenticateWithSessionCookieSuccessResponse,
|
|
15
|
+
AuthenticateWithSessionCookieErrorResponse,
|
|
16
|
+
RefreshWithSessionCookieErrorResponse,
|
|
17
|
+
RefreshWithSessionCookieSuccessResponse,
|
|
18
|
+
)
|
|
19
|
+
from workos.typing.sync_or_async import SyncOrAsync
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from workos.user_management import UserManagementModule
|
|
23
|
+
from workos.user_management import AsyncUserManagement, UserManagement
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=None)
|
|
27
|
+
def _get_jwks_client(jwks_url: str) -> PyJWKClient:
|
|
28
|
+
return PyJWKClient(jwks_url)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SessionModule(Protocol):
|
|
32
|
+
user_management: "UserManagementModule"
|
|
33
|
+
client_id: str
|
|
34
|
+
session_data: str
|
|
35
|
+
cookie_password: str
|
|
36
|
+
jwks: PyJWKClient
|
|
37
|
+
jwk_algorithms: List[str]
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
user_management: "UserManagementModule",
|
|
43
|
+
client_id: str,
|
|
44
|
+
session_data: str,
|
|
45
|
+
cookie_password: str,
|
|
46
|
+
) -> None:
|
|
47
|
+
# If the cookie password is not provided, throw an error
|
|
48
|
+
if cookie_password is None or cookie_password == "":
|
|
49
|
+
raise ValueError("cookie_password is required")
|
|
50
|
+
|
|
51
|
+
self.user_management = user_management
|
|
52
|
+
self.client_id = client_id
|
|
53
|
+
self.session_data = session_data
|
|
54
|
+
self.cookie_password = cookie_password
|
|
55
|
+
|
|
56
|
+
self.jwks = _get_jwks_client(self.user_management.get_jwks_url())
|
|
57
|
+
|
|
58
|
+
# Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm
|
|
59
|
+
self.jwk_algorithms = ["RS256"]
|
|
60
|
+
|
|
61
|
+
def authenticate(
|
|
62
|
+
self,
|
|
63
|
+
) -> Union[
|
|
64
|
+
AuthenticateWithSessionCookieSuccessResponse,
|
|
65
|
+
AuthenticateWithSessionCookieErrorResponse,
|
|
66
|
+
]:
|
|
67
|
+
if self.session_data is None or self.session_data == "":
|
|
68
|
+
return AuthenticateWithSessionCookieErrorResponse(
|
|
69
|
+
authenticated=False,
|
|
70
|
+
reason=AuthenticateWithSessionCookieFailureReason.NO_SESSION_COOKIE_PROVIDED,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
session = self.unseal_data(self.session_data, self.cookie_password)
|
|
75
|
+
except Exception:
|
|
76
|
+
return AuthenticateWithSessionCookieErrorResponse(
|
|
77
|
+
authenticated=False,
|
|
78
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if not session.get("access_token", None):
|
|
82
|
+
return AuthenticateWithSessionCookieErrorResponse(
|
|
83
|
+
authenticated=False,
|
|
84
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
signing_key = self.jwks.get_signing_key_from_jwt(session["access_token"])
|
|
89
|
+
decoded = jwt.decode(
|
|
90
|
+
session["access_token"],
|
|
91
|
+
signing_key.key,
|
|
92
|
+
algorithms=self.jwk_algorithms,
|
|
93
|
+
options={"verify_aud": False},
|
|
94
|
+
)
|
|
95
|
+
except jwt.exceptions.InvalidTokenError:
|
|
96
|
+
return AuthenticateWithSessionCookieErrorResponse(
|
|
97
|
+
authenticated=False,
|
|
98
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_JWT,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return AuthenticateWithSessionCookieSuccessResponse(
|
|
102
|
+
authenticated=True,
|
|
103
|
+
session_id=decoded["sid"],
|
|
104
|
+
organization_id=decoded.get("org_id", None),
|
|
105
|
+
role=decoded.get("role", None),
|
|
106
|
+
roles=decoded.get("roles", None),
|
|
107
|
+
permissions=decoded.get("permissions", None),
|
|
108
|
+
entitlements=decoded.get("entitlements", None),
|
|
109
|
+
user=session["user"],
|
|
110
|
+
impersonator=session.get("impersonator", None),
|
|
111
|
+
feature_flags=decoded.get("feature_flags", None),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def refresh(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
organization_id: Optional[str] = None,
|
|
118
|
+
cookie_password: Optional[str] = None,
|
|
119
|
+
) -> SyncOrAsync[
|
|
120
|
+
Union[
|
|
121
|
+
RefreshWithSessionCookieSuccessResponse,
|
|
122
|
+
RefreshWithSessionCookieErrorResponse,
|
|
123
|
+
]
|
|
124
|
+
]: ...
|
|
125
|
+
|
|
126
|
+
def get_logout_url(self, return_to: Optional[str] = None) -> str:
|
|
127
|
+
auth_response = self.authenticate()
|
|
128
|
+
|
|
129
|
+
if isinstance(auth_response, AuthenticateWithSessionCookieErrorResponse):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Failed to extract session ID for logout URL: {auth_response.reason}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
result = self.user_management.get_logout_url(
|
|
135
|
+
session_id=auth_response.session_id,
|
|
136
|
+
return_to=return_to,
|
|
137
|
+
)
|
|
138
|
+
return str(result)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def seal_data(data: Dict[str, Any], key: str) -> str:
|
|
142
|
+
fernet = Fernet(key)
|
|
143
|
+
# Encrypt and convert bytes to string
|
|
144
|
+
encrypted_bytes = fernet.encrypt(json.dumps(data).encode())
|
|
145
|
+
return encrypted_bytes.decode("utf-8")
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def unseal_data(sealed_data: str, key: str) -> Dict[str, Any]:
|
|
149
|
+
fernet = Fernet(key)
|
|
150
|
+
# Convert string back to bytes before decryption
|
|
151
|
+
encrypted_bytes = sealed_data.encode("utf-8")
|
|
152
|
+
decrypted_str = fernet.decrypt(encrypted_bytes).decode()
|
|
153
|
+
return cast(Dict[str, Any], json.loads(decrypted_str))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Session(SessionModule):
|
|
157
|
+
user_management: "UserManagement"
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
user_management: "UserManagement",
|
|
163
|
+
client_id: str,
|
|
164
|
+
session_data: str,
|
|
165
|
+
cookie_password: str,
|
|
166
|
+
) -> None:
|
|
167
|
+
# If the cookie password is not provided, throw an error
|
|
168
|
+
if cookie_password is None or cookie_password == "":
|
|
169
|
+
raise ValueError("cookie_password is required")
|
|
170
|
+
|
|
171
|
+
self.user_management = user_management
|
|
172
|
+
self.client_id = client_id
|
|
173
|
+
self.session_data = session_data
|
|
174
|
+
self.cookie_password = cookie_password
|
|
175
|
+
|
|
176
|
+
self.jwks = _get_jwks_client(self.user_management.get_jwks_url())
|
|
177
|
+
|
|
178
|
+
# Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm
|
|
179
|
+
self.jwk_algorithms = ["RS256"]
|
|
180
|
+
|
|
181
|
+
def refresh(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
organization_id: Optional[str] = None,
|
|
185
|
+
cookie_password: Optional[str] = None,
|
|
186
|
+
) -> Union[
|
|
187
|
+
RefreshWithSessionCookieSuccessResponse,
|
|
188
|
+
RefreshWithSessionCookieErrorResponse,
|
|
189
|
+
]:
|
|
190
|
+
cookie_password = (
|
|
191
|
+
self.cookie_password if cookie_password is None else cookie_password
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
session = self.unseal_data(self.session_data, cookie_password)
|
|
196
|
+
except Exception:
|
|
197
|
+
return RefreshWithSessionCookieErrorResponse(
|
|
198
|
+
authenticated=False,
|
|
199
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not session.get("refresh_token", None) or not session.get("user", None):
|
|
203
|
+
return RefreshWithSessionCookieErrorResponse(
|
|
204
|
+
authenticated=False,
|
|
205
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
auth_response = self.user_management.authenticate_with_refresh_token(
|
|
210
|
+
refresh_token=session["refresh_token"],
|
|
211
|
+
organization_id=organization_id,
|
|
212
|
+
session={"seal_session": True, "cookie_password": cookie_password},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
self.session_data = str(auth_response.sealed_session)
|
|
216
|
+
self.cookie_password = (
|
|
217
|
+
cookie_password if cookie_password is not None else self.cookie_password
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
signing_key = self.jwks.get_signing_key_from_jwt(auth_response.access_token)
|
|
221
|
+
|
|
222
|
+
decoded = jwt.decode(
|
|
223
|
+
auth_response.access_token,
|
|
224
|
+
signing_key.key,
|
|
225
|
+
algorithms=self.jwk_algorithms,
|
|
226
|
+
options={"verify_aud": False},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return RefreshWithSessionCookieSuccessResponse(
|
|
230
|
+
authenticated=True,
|
|
231
|
+
sealed_session=str(auth_response.sealed_session),
|
|
232
|
+
session_id=decoded["sid"],
|
|
233
|
+
organization_id=decoded.get("org_id", None),
|
|
234
|
+
role=decoded.get("role", None),
|
|
235
|
+
roles=decoded.get("roles", None),
|
|
236
|
+
permissions=decoded.get("permissions", None),
|
|
237
|
+
entitlements=decoded.get("entitlements", None),
|
|
238
|
+
user=auth_response.user,
|
|
239
|
+
impersonator=auth_response.impersonator,
|
|
240
|
+
feature_flags=decoded.get("feature_flags", None),
|
|
241
|
+
)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return RefreshWithSessionCookieErrorResponse(
|
|
244
|
+
authenticated=False, reason=str(e)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class AsyncSession(SessionModule):
|
|
249
|
+
user_management: "AsyncUserManagement"
|
|
250
|
+
|
|
251
|
+
def __init__(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
user_management: "AsyncUserManagement",
|
|
255
|
+
client_id: str,
|
|
256
|
+
session_data: str,
|
|
257
|
+
cookie_password: str,
|
|
258
|
+
) -> None:
|
|
259
|
+
# If the cookie password is not provided, throw an error
|
|
260
|
+
if cookie_password is None or cookie_password == "":
|
|
261
|
+
raise ValueError("cookie_password is required")
|
|
262
|
+
|
|
263
|
+
self.user_management = user_management
|
|
264
|
+
self.client_id = client_id
|
|
265
|
+
self.session_data = session_data
|
|
266
|
+
self.cookie_password = cookie_password
|
|
267
|
+
|
|
268
|
+
self.jwks = _get_jwks_client(self.user_management.get_jwks_url())
|
|
269
|
+
|
|
270
|
+
# Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm
|
|
271
|
+
self.jwk_algorithms = ["RS256"]
|
|
272
|
+
|
|
273
|
+
async def refresh(
|
|
274
|
+
self,
|
|
275
|
+
*,
|
|
276
|
+
organization_id: Optional[str] = None,
|
|
277
|
+
cookie_password: Optional[str] = None,
|
|
278
|
+
) -> Union[
|
|
279
|
+
RefreshWithSessionCookieSuccessResponse,
|
|
280
|
+
RefreshWithSessionCookieErrorResponse,
|
|
281
|
+
]:
|
|
282
|
+
cookie_password = (
|
|
283
|
+
self.cookie_password if cookie_password is None else cookie_password
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
session = self.unseal_data(self.session_data, cookie_password)
|
|
288
|
+
except Exception:
|
|
289
|
+
return RefreshWithSessionCookieErrorResponse(
|
|
290
|
+
authenticated=False,
|
|
291
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if not session.get("refresh_token", None) or not session.get("user", None):
|
|
295
|
+
return RefreshWithSessionCookieErrorResponse(
|
|
296
|
+
authenticated=False,
|
|
297
|
+
reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
auth_response = await self.user_management.authenticate_with_refresh_token(
|
|
302
|
+
refresh_token=session["refresh_token"],
|
|
303
|
+
organization_id=organization_id,
|
|
304
|
+
session={"seal_session": True, "cookie_password": cookie_password},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
self.session_data = str(auth_response.sealed_session)
|
|
308
|
+
self.cookie_password = (
|
|
309
|
+
cookie_password if cookie_password is not None else self.cookie_password
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
signing_key = self.jwks.get_signing_key_from_jwt(auth_response.access_token)
|
|
313
|
+
|
|
314
|
+
decoded = jwt.decode(
|
|
315
|
+
auth_response.access_token,
|
|
316
|
+
signing_key.key,
|
|
317
|
+
algorithms=self.jwk_algorithms,
|
|
318
|
+
options={"verify_aud": False},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return RefreshWithSessionCookieSuccessResponse(
|
|
322
|
+
authenticated=True,
|
|
323
|
+
sealed_session=str(auth_response.sealed_session),
|
|
324
|
+
session_id=decoded["sid"],
|
|
325
|
+
organization_id=decoded.get("org_id", None),
|
|
326
|
+
role=decoded.get("role", None),
|
|
327
|
+
roles=decoded.get("roles", None),
|
|
328
|
+
permissions=decoded.get("permissions", None),
|
|
329
|
+
entitlements=decoded.get("entitlements", None),
|
|
330
|
+
user=auth_response.user,
|
|
331
|
+
impersonator=auth_response.impersonator,
|
|
332
|
+
feature_flags=decoded.get("feature_flags", None),
|
|
333
|
+
)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
return RefreshWithSessionCookieErrorResponse(
|
|
336
|
+
authenticated=False, reason=str(e)
|
|
337
|
+
)
|