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.
Files changed (150) hide show
  1. workos/__about__.py +1 -1
  2. workos/__init__.py +3 -7
  3. workos/_base_client.py +138 -0
  4. workos/_client_configuration.py +10 -0
  5. workos/api_keys.py +53 -0
  6. workos/async_client.py +144 -0
  7. workos/audit_logs.py +125 -0
  8. workos/client.py +110 -18
  9. workos/directory_sync.py +379 -99
  10. workos/events.py +111 -0
  11. workos/exceptions.py +53 -26
  12. workos/fga.py +649 -0
  13. workos/mfa.py +205 -0
  14. workos/organization_domains.py +179 -0
  15. workos/organizations.py +403 -73
  16. workos/passwordless.py +67 -43
  17. workos/pipes.py +93 -0
  18. workos/portal.py +51 -28
  19. workos/session.py +337 -0
  20. workos/sso.py +311 -101
  21. workos/types/__init__.py +4 -0
  22. workos/types/api_keys/__init__.py +1 -0
  23. workos/types/api_keys/api_keys.py +20 -0
  24. workos/types/audit_logs/__init__.py +6 -0
  25. workos/types/audit_logs/audit_log_event.py +16 -0
  26. workos/types/audit_logs/audit_log_event_actor.py +12 -0
  27. workos/types/audit_logs/audit_log_event_context.py +8 -0
  28. workos/types/audit_logs/audit_log_event_target.py +12 -0
  29. workos/types/audit_logs/audit_log_export.py +18 -0
  30. workos/types/audit_logs/audit_log_metadata.py +4 -0
  31. workos/types/directory_sync/__init__.py +5 -0
  32. workos/types/directory_sync/directory.py +31 -0
  33. workos/types/directory_sync/directory_group.py +16 -0
  34. workos/types/directory_sync/directory_state.py +28 -0
  35. workos/types/directory_sync/directory_type.py +24 -0
  36. workos/types/directory_sync/directory_user.py +50 -0
  37. workos/types/directory_sync/list_filters.py +21 -0
  38. workos/types/events/__init__.py +13 -0
  39. workos/types/events/authentication_payload.py +70 -0
  40. workos/types/events/connection_payload_with_legacy_fields.py +5 -0
  41. workos/types/events/directory_group_membership_payload.py +9 -0
  42. workos/types/events/directory_group_with_previous_attributes.py +6 -0
  43. workos/types/events/directory_payload.py +16 -0
  44. workos/types/events/directory_payload_with_legacy_fields.py +29 -0
  45. workos/types/events/directory_user_with_previous_attributes.py +6 -0
  46. workos/types/events/event.py +324 -0
  47. workos/types/events/event_model.py +103 -0
  48. workos/types/events/event_type.py +59 -0
  49. workos/types/events/list_filters.py +10 -0
  50. workos/types/events/organization_domain_verification_failed_payload.py +14 -0
  51. workos/types/events/previous_attributes.py +3 -0
  52. workos/types/events/session_payload.py +27 -0
  53. workos/types/feature_flags/__init__.py +3 -0
  54. workos/types/feature_flags/feature_flag.py +12 -0
  55. workos/types/feature_flags/list_filters.py +5 -0
  56. workos/types/fga/__init__.py +5 -0
  57. workos/types/fga/authorization_resource_types.py +9 -0
  58. workos/types/fga/authorization_resources.py +10 -0
  59. workos/types/fga/check.py +51 -0
  60. workos/types/fga/list_filters.py +24 -0
  61. workos/types/fga/warnings.py +33 -0
  62. workos/types/fga/warrant.py +49 -0
  63. workos/types/list_resource.py +198 -0
  64. workos/types/metadata.py +4 -0
  65. workos/types/mfa/__init__.py +5 -0
  66. workos/types/mfa/authentication_challenge.py +14 -0
  67. workos/types/mfa/authentication_challenge_verification_response.py +9 -0
  68. workos/types/mfa/authentication_factor.py +70 -0
  69. workos/types/mfa/authentication_factor_totp_and_challenge_response.py +10 -0
  70. workos/types/mfa/enroll_authentication_factor_type.py +8 -0
  71. workos/types/organization_domains/__init__.py +1 -0
  72. workos/types/organization_domains/organization_domain.py +18 -0
  73. workos/types/organizations/__init__.py +6 -0
  74. workos/types/organizations/domain_data_input.py +7 -0
  75. workos/types/organizations/list_filters.py +6 -0
  76. workos/types/organizations/organization.py +13 -0
  77. workos/types/organizations/organization_common.py +12 -0
  78. workos/types/passwordless/__init__.py +2 -0
  79. workos/types/passwordless/passwordless_session.py +12 -0
  80. workos/types/passwordless/passwordless_session_type.py +3 -0
  81. workos/types/pipes/__init__.py +6 -0
  82. workos/types/pipes/pipes.py +34 -0
  83. workos/types/portal/__init__.py +2 -0
  84. workos/types/portal/portal_link.py +7 -0
  85. workos/types/portal/portal_link_intent.py +11 -0
  86. workos/types/portal/portal_link_intent_options.py +9 -0
  87. workos/types/roles/__init__.py +0 -0
  88. workos/types/roles/role.py +27 -0
  89. workos/types/sso/__init__.py +4 -0
  90. workos/types/sso/connection.py +70 -0
  91. workos/types/sso/connection_domain.py +8 -0
  92. workos/types/sso/profile.py +35 -0
  93. workos/types/sso/sso_provider_type.py +10 -0
  94. workos/types/user_management/__init__.py +12 -0
  95. workos/types/user_management/authenticate_with_common.py +66 -0
  96. workos/types/user_management/authentication_response.py +53 -0
  97. workos/types/user_management/email_verification.py +18 -0
  98. workos/types/user_management/impersonator.py +8 -0
  99. workos/types/user_management/invitation.py +26 -0
  100. workos/types/user_management/list_filters.py +29 -0
  101. workos/types/user_management/magic_auth.py +18 -0
  102. workos/types/user_management/oauth_tokens.py +21 -0
  103. workos/types/user_management/organization_membership.py +25 -0
  104. workos/types/user_management/password_hash_type.py +4 -0
  105. workos/types/user_management/password_reset.py +18 -0
  106. workos/types/user_management/screen_hint.py +3 -0
  107. workos/types/user_management/session.py +79 -0
  108. workos/types/user_management/user.py +22 -0
  109. workos/types/user_management/user_management_provider_type.py +11 -0
  110. workos/types/vault/__init__.py +2 -0
  111. workos/types/vault/key.py +25 -0
  112. workos/types/vault/object.py +38 -0
  113. workos/types/webhooks/__init__.py +0 -0
  114. workos/types/webhooks/webhook.py +330 -0
  115. workos/types/webhooks/webhook_model.py +14 -0
  116. workos/types/webhooks/webhook_payload.py +4 -0
  117. workos/types/widgets/__init__.py +2 -0
  118. workos/types/widgets/widget_scope.py +4 -0
  119. workos/types/widgets/widget_token_response.py +7 -0
  120. workos/types/workos_model.py +26 -0
  121. workos/typing/__init__.py +1 -0
  122. workos/typing/literals.py +32 -0
  123. workos/typing/sync_or_async.py +5 -0
  124. workos/typing/untyped_literal.py +37 -0
  125. workos/typing/webhooks.py +18 -0
  126. workos/user_management.py +2400 -0
  127. workos/utils/_base_http_client.py +252 -0
  128. workos/utils/crypto_provider.py +39 -0
  129. workos/utils/http_client.py +214 -0
  130. workos/utils/pagination_order.py +4 -0
  131. workos/utils/request_helper.py +27 -0
  132. workos/vault.py +544 -0
  133. workos/webhooks.py +96 -39
  134. workos/widgets.py +55 -0
  135. {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/LICENSE +1 -1
  136. workos-5.38.0.dist-info/METADATA +107 -0
  137. workos-5.38.0.dist-info/RECORD +141 -0
  138. {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/WHEEL +1 -1
  139. workos/audit_trail.py +0 -172
  140. workos/resources/base.py +0 -36
  141. workos/resources/event.py +0 -42
  142. workos/resources/event_action.py +0 -11
  143. workos/resources/sso.py +0 -53
  144. workos/utils/connection_types.py +0 -17
  145. workos/utils/request.py +0 -95
  146. workos/utils/validation.py +0 -45
  147. workos-1.5.1.dist-info/METADATA +0 -77
  148. workos-1.5.1.dist-info/RECORD +0 -25
  149. /workos/{resources/__init__.py → py.typed} +0 -0
  150. {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 workos
2
- from workos.utils.request import RequestHelper, REQUEST_METHOD_POST
3
- from workos.utils.validation import PORTAL_MODULE, validate_settings
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 Portal(object):
10
- @validate_settings(PORTAL_MODULE)
11
- def __init__(self):
12
- pass
13
-
14
- @property
15
- def request_helper(self):
16
- if not getattr(self, "_request_helper", None):
17
- self._request_helper = RequestHelper()
18
- return self._request_helper
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
- return_url (str): The URL that the end user will be redirected to upon exiting the generated Admin Portal. If none is provided, the default redirect link set in your WorkOS Dashboard will be used. (Optional)
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
- str: URL to redirect a User to to access an Admin Portal session
33
+ PortalLink: PortalLink object with URL to redirect a User to to access an Admin Portal session.
32
34
  """
33
- params = {
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": organization,
56
+ "organization": organization_id,
36
57
  "return_url": return_url,
58
+ "success_url": success_url,
59
+ "intent_options": intent_options,
37
60
  }
38
- return self.request_helper.request(
39
- PORTAL_GENERATE_PATH,
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
+ )