auth0-server-python 1.0.0b7__tar.gz → 1.0.0b8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/PKG-INFO +42 -1
  2. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/README.md +41 -0
  3. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/pyproject.toml +1 -1
  4. auth0_server_python-1.0.0b8/src/auth0_server_python/auth_server/my_account_client.py +334 -0
  5. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_server/server_client.py +834 -449
  6. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_types/__init__.py +156 -10
  7. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/error/__init__.py +31 -0
  8. auth0_server_python-1.0.0b8/src/auth0_server_python/tests/test_my_account_client.py +503 -0
  9. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/tests/test_server_client.py +892 -0
  10. auth0_server_python-1.0.0b7/src/auth0_server_python/auth_server/my_account_client.py +0 -94
  11. auth0_server_python-1.0.0b7/src/auth0_server_python/tests/test_my_account_client.py +0 -160
  12. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/LICENSE +0 -0
  13. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
  14. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
  15. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_server/__init__.py +0 -0
  16. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/encryption/__init__.py +0 -0
  17. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/encryption/encrypt.py +0 -0
  18. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/store/__init__.py +0 -0
  19. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/store/abstract.py +0 -0
  20. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/utils/__init__.py +0 -0
  21. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/utils/helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth0-server-python
3
- Version: 1.0.0b7
3
+ Version: 1.0.0b8
4
4
  Summary: Auth0 server-side Python SDK
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -129,6 +129,47 @@ async def callback(request: Request):
129
129
  return RedirectResponse(url="/")
130
130
  ```
131
131
 
132
+ ### 4. Login with Custom Token Exchange
133
+
134
+ If you're migrating from a legacy authentication system or integrating with a custom identity provider, you can exchange external tokens for Auth0 tokens using the OAuth 2.0 Token Exchange specification (RFC 8693):
135
+
136
+ ```python
137
+ from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
138
+
139
+ # Exchange a custom token and establish a session
140
+ result = await auth0.login_with_custom_token_exchange(
141
+ LoginWithCustomTokenExchangeOptions(
142
+ subject_token="your-custom-token",
143
+ subject_token_type="urn:acme:mcp-token",
144
+ audience="https://api.example.com"
145
+ ),
146
+ store_options={"request": request, "response": response}
147
+ )
148
+
149
+ # Access the user session
150
+ user = result.state_data["user"]
151
+ ```
152
+
153
+ For advanced token exchange scenarios (without creating a session), use `custom_token_exchange()` directly:
154
+
155
+ ```python
156
+ from auth0_server_python.auth_types import CustomTokenExchangeOptions
157
+
158
+ # Exchange a custom token for Auth0 tokens
159
+ response = await auth0.custom_token_exchange(
160
+ CustomTokenExchangeOptions(
161
+ subject_token="your-custom-token",
162
+ subject_token_type="urn:acme:mcp-token",
163
+ audience="https://api.example.com",
164
+ scope="read:data write:data"
165
+ )
166
+ )
167
+
168
+ print(response.access_token)
169
+ ```
170
+
171
+ For more details and examples, see [examples/CustomTokenExchange.md](examples/CustomTokenExchange.md).
172
+
132
173
  ## Feedback
133
174
 
134
175
  ### Contributing
@@ -104,6 +104,47 @@ async def callback(request: Request):
104
104
  return RedirectResponse(url="/")
105
105
  ```
106
106
 
107
+ ### 4. Login with Custom Token Exchange
108
+
109
+ If you're migrating from a legacy authentication system or integrating with a custom identity provider, you can exchange external tokens for Auth0 tokens using the OAuth 2.0 Token Exchange specification (RFC 8693):
110
+
111
+ ```python
112
+ from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
113
+
114
+ # Exchange a custom token and establish a session
115
+ result = await auth0.login_with_custom_token_exchange(
116
+ LoginWithCustomTokenExchangeOptions(
117
+ subject_token="your-custom-token",
118
+ subject_token_type="urn:acme:mcp-token",
119
+ audience="https://api.example.com"
120
+ ),
121
+ store_options={"request": request, "response": response}
122
+ )
123
+
124
+ # Access the user session
125
+ user = result.state_data["user"]
126
+ ```
127
+
128
+ For advanced token exchange scenarios (without creating a session), use `custom_token_exchange()` directly:
129
+
130
+ ```python
131
+ from auth0_server_python.auth_types import CustomTokenExchangeOptions
132
+
133
+ # Exchange a custom token for Auth0 tokens
134
+ response = await auth0.custom_token_exchange(
135
+ CustomTokenExchangeOptions(
136
+ subject_token="your-custom-token",
137
+ subject_token_type="urn:acme:mcp-token",
138
+ audience="https://api.example.com",
139
+ scope="read:data write:data"
140
+ )
141
+ )
142
+
143
+ print(response.access_token)
144
+ ```
145
+
146
+ For more details and examples, see [examples/CustomTokenExchange.md](examples/CustomTokenExchange.md).
147
+
107
148
  ## Feedback
108
149
 
109
150
  ### Contributing
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0.b7"
3
+ version = "1.0.0.b8"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -0,0 +1,334 @@
1
+
2
+ from typing import Optional
3
+
4
+ import httpx
5
+ from auth0_server_python.auth_schemes.bearer_auth import BearerAuth
6
+ from auth0_server_python.auth_types import (
7
+ CompleteConnectAccountRequest,
8
+ CompleteConnectAccountResponse,
9
+ ConnectAccountRequest,
10
+ ConnectAccountResponse,
11
+ ListConnectedAccountConnectionsResponse,
12
+ ListConnectedAccountsResponse,
13
+ )
14
+ from auth0_server_python.error import (
15
+ ApiError,
16
+ InvalidArgumentError,
17
+ MissingRequiredArgumentError,
18
+ MyAccountApiError,
19
+ )
20
+
21
+
22
+ class MyAccountClient:
23
+ """
24
+ Client for interacting with the Auth0 MyAccount API.
25
+ """
26
+
27
+ def __init__(self, domain: str):
28
+ """
29
+ Initialize the MyAccount API client.
30
+
31
+ Args:
32
+ domain: Auth0 domain (e.g., '<tenant>.<locality>.auth0.com')
33
+ """
34
+ self._domain = domain
35
+
36
+ @property
37
+ def audience(self):
38
+ """
39
+ Get the MyAccount API audience URL.
40
+
41
+ Returns:
42
+ The audience URL for the MyAccount API
43
+ """
44
+ return f"https://{self._domain}/me/"
45
+
46
+ async def connect_account(
47
+ self,
48
+ access_token: str,
49
+ request: ConnectAccountRequest
50
+ ) -> ConnectAccountResponse:
51
+ """
52
+ Initiate the connected account flow.
53
+
54
+ Args:
55
+ access_token: User's access token for authentication
56
+ request: Request containing connection details and configuration
57
+
58
+ Returns:
59
+ Response containing the connect URI and authentication session details
60
+
61
+ Raises:
62
+ MyAccountApiError: If the API returns an error response
63
+ ApiError: If the request fails due to network or other issues
64
+ """
65
+ try:
66
+ async with httpx.AsyncClient() as client:
67
+ response = await client.post(
68
+ url=f"{self.audience}v1/connected-accounts/connect",
69
+ json=request.model_dump(exclude_none=True),
70
+ auth=BearerAuth(access_token)
71
+ )
72
+
73
+ if response.status_code != 201:
74
+ error_data = response.json()
75
+ raise MyAccountApiError(
76
+ title=error_data.get("title", None),
77
+ type=error_data.get("type", None),
78
+ detail=error_data.get("detail", None),
79
+ status=error_data.get("status", None),
80
+ validation_errors=error_data.get("validation_errors", None)
81
+ )
82
+
83
+ data = response.json()
84
+
85
+ return ConnectAccountResponse.model_validate(data)
86
+
87
+ except Exception as e:
88
+ if isinstance(e, MyAccountApiError):
89
+ raise
90
+ raise ApiError(
91
+ "connect_account_error",
92
+ f"Connected Accounts connect request failed: {str(e) or 'Unknown error'}",
93
+ e
94
+ )
95
+
96
+ async def complete_connect_account(
97
+ self,
98
+ access_token: str,
99
+ request: CompleteConnectAccountRequest
100
+ ) -> CompleteConnectAccountResponse:
101
+ """
102
+ Complete the connected account flow after user authorization.
103
+
104
+ Args:
105
+ access_token: User's access token for authentication
106
+ request: Request containing the auth session, connect code, and redirect URI
107
+
108
+ Returns:
109
+ Response containing the connected account details including ID, connection, and scopes
110
+
111
+ Raises:
112
+ MyAccountApiError: If the API returns an error response
113
+ ApiError: If the request fails due to network or other issues
114
+ """
115
+ try:
116
+ async with httpx.AsyncClient() as client:
117
+ response = await client.post(
118
+ url=f"{self.audience}v1/connected-accounts/complete",
119
+ json=request.model_dump(exclude_none=True),
120
+ auth=BearerAuth(access_token)
121
+ )
122
+
123
+ if response.status_code != 201:
124
+ error_data = response.json()
125
+ raise MyAccountApiError(
126
+ title=error_data.get("title", None),
127
+ type=error_data.get("type", None),
128
+ detail=error_data.get("detail", None),
129
+ status=error_data.get("status", None),
130
+ validation_errors=error_data.get("validation_errors", None)
131
+ )
132
+
133
+ data = response.json()
134
+
135
+ return CompleteConnectAccountResponse.model_validate(data)
136
+
137
+ except Exception as e:
138
+ if isinstance(e, MyAccountApiError):
139
+ raise
140
+ raise ApiError(
141
+ "connect_account_error",
142
+ f"Connected Accounts complete request failed: {str(e) or 'Unknown error'}",
143
+ e
144
+ )
145
+
146
+ async def list_connected_accounts(
147
+ self,
148
+ access_token: str,
149
+ connection: Optional[str] = None,
150
+ from_param: Optional[str] = None,
151
+ take: Optional[int] = None
152
+ ) -> ListConnectedAccountsResponse:
153
+ """
154
+ List connected accounts for the authenticated user.
155
+
156
+ Args:
157
+ access_token: User's access token for authentication
158
+ connection: Optional filter to list accounts for a specific connection
159
+ from_param: Optional pagination cursor for fetching next page of results
160
+ take: Optional number of results to return (must be a positive integer)
161
+
162
+ Returns:
163
+ Response containing the list of connected accounts and pagination details
164
+
165
+ Raises:
166
+ MissingRequiredArgumentError: If access_token is not provided
167
+ InvalidArgumentError: If take parameter is not a positive integer
168
+ MyAccountApiError: If the API returns an error response
169
+ ApiError: If the request fails due to network or other issues
170
+ """
171
+ if access_token is None:
172
+ raise MissingRequiredArgumentError("access_token")
173
+
174
+ if take is not None and (not isinstance(take, int) or take < 1):
175
+ raise InvalidArgumentError("take", "The 'take' parameter must be a positive integer.")
176
+
177
+ try:
178
+ async with httpx.AsyncClient() as client:
179
+ params = {}
180
+ if connection:
181
+ params["connection"] = connection
182
+ if from_param:
183
+ params["from"] = from_param
184
+ if take:
185
+ params["take"] = take
186
+
187
+ response = await client.get(
188
+ url=f"{self.audience}v1/connected-accounts/accounts",
189
+ params=params,
190
+ auth=BearerAuth(access_token)
191
+ )
192
+
193
+ if response.status_code != 200:
194
+ error_data = response.json()
195
+ raise MyAccountApiError(
196
+ title=error_data.get("title", None),
197
+ type=error_data.get("type", None),
198
+ detail=error_data.get("detail", None),
199
+ status=error_data.get("status", None),
200
+ validation_errors=error_data.get("validation_errors", None)
201
+ )
202
+
203
+ data = response.json()
204
+
205
+ return ListConnectedAccountsResponse.model_validate(data)
206
+
207
+ except Exception as e:
208
+ if isinstance(e, MyAccountApiError):
209
+ raise
210
+ raise ApiError(
211
+ "connect_account_error",
212
+ f"Connected Accounts list request failed: {str(e) or 'Unknown error'}",
213
+ e
214
+ )
215
+
216
+
217
+ async def delete_connected_account(
218
+ self,
219
+ access_token: str,
220
+ connected_account_id: str
221
+ ) -> None:
222
+ """
223
+ Delete a connected account for the authenticated user.
224
+
225
+ Args:
226
+ access_token: User's access token for authentication
227
+ connected_account_id: ID of the connected account to delete
228
+
229
+ Returns:
230
+ None
231
+
232
+ Raises:
233
+ MissingRequiredArgumentError: If access_token or connected_account_id is not provided
234
+ MyAccountApiError: If the API returns an error response
235
+ ApiError: If the request fails due to network or other issues
236
+ """
237
+
238
+ if access_token is None:
239
+ raise MissingRequiredArgumentError("access_token")
240
+
241
+ if connected_account_id is None:
242
+ raise MissingRequiredArgumentError("connected_account_id")
243
+
244
+ try:
245
+ async with httpx.AsyncClient() as client:
246
+ response = await client.delete(
247
+ url=f"{self.audience}v1/connected-accounts/accounts/{connected_account_id}",
248
+ auth=BearerAuth(access_token)
249
+ )
250
+
251
+ if response.status_code != 204:
252
+ error_data = response.json()
253
+ raise MyAccountApiError(
254
+ title=error_data.get("title", None),
255
+ type=error_data.get("type", None),
256
+ detail=error_data.get("detail", None),
257
+ status=error_data.get("status", None),
258
+ validation_errors=error_data.get("validation_errors", None)
259
+ )
260
+
261
+ except Exception as e:
262
+ if isinstance(e, MyAccountApiError):
263
+ raise
264
+ raise ApiError(
265
+ "connect_account_error",
266
+ f"Connected Accounts delete request failed: {str(e) or 'Unknown error'}",
267
+ e
268
+ )
269
+
270
+ async def list_connected_account_connections(
271
+ self,
272
+ access_token: str,
273
+ from_param: Optional[str] = None,
274
+ take: Optional[int] = None
275
+ ) -> ListConnectedAccountConnectionsResponse:
276
+ """
277
+ List available connections that support connected accounts.
278
+
279
+ Args:
280
+ access_token: User's access token for authentication
281
+ from_param: Optional pagination cursor for fetching next page of results
282
+ take: Optional number of results to return (must be a positive integer)
283
+
284
+ Returns:
285
+ Response containing the list of available connections and pagination details
286
+
287
+ Raises:
288
+ MissingRequiredArgumentError: If access_token is not provided
289
+ InvalidArgumentError: If take parameter is not a positive integer
290
+ MyAccountApiError: If the API returns an error response
291
+ ApiError: If the request fails due to network or other issues
292
+ """
293
+ if access_token is None:
294
+ raise MissingRequiredArgumentError("access_token")
295
+
296
+ if take is not None and (not isinstance(take, int) or take < 1):
297
+ raise InvalidArgumentError("take", "The 'take' parameter must be a positive integer.")
298
+
299
+ try:
300
+ async with httpx.AsyncClient() as client:
301
+ params = {}
302
+ if from_param:
303
+ params["from"] = from_param
304
+ if take:
305
+ params["take"] = take
306
+
307
+ response = await client.get(
308
+ url=f"{self.audience}v1/connected-accounts/connections",
309
+ params=params,
310
+ auth=BearerAuth(access_token)
311
+ )
312
+
313
+ if response.status_code != 200:
314
+ error_data = response.json()
315
+ raise MyAccountApiError(
316
+ title=error_data.get("title", None),
317
+ type=error_data.get("type", None),
318
+ detail=error_data.get("detail", None),
319
+ status=error_data.get("status", None),
320
+ validation_errors=error_data.get("validation_errors", None)
321
+ )
322
+
323
+ data = response.json()
324
+
325
+ return ListConnectedAccountConnectionsResponse.model_validate(data)
326
+
327
+ except Exception as e:
328
+ if isinstance(e, MyAccountApiError):
329
+ raise
330
+ raise ApiError(
331
+ "connect_account_error",
332
+ f"Connected Accounts list connections request failed: {str(e) or 'Unknown error'}",
333
+ e
334
+ )