auth0-server-python 1.0.0b7__tar.gz → 1.0.0b9__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 (22) hide show
  1. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/PKG-INFO +71 -2
  2. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/README.md +69 -0
  3. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/pyproject.toml +3 -3
  4. auth0_server_python-1.0.0b9/src/auth0_server_python/auth_server/my_account_client.py +335 -0
  5. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/auth_server/server_client.py +1433 -491
  6. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/auth_types/__init__.py +182 -10
  7. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/error/__init__.py +74 -0
  8. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/store/abstract.py +15 -2
  9. auth0_server_python-1.0.0b9/src/auth0_server_python/tests/test_my_account_client.py +504 -0
  10. auth0_server_python-1.0.0b9/src/auth0_server_python/tests/test_server_client.py +4594 -0
  11. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/utils/helpers.py +69 -0
  12. auth0_server_python-1.0.0b7/src/auth0_server_python/auth_server/my_account_client.py +0 -94
  13. auth0_server_python-1.0.0b7/src/auth0_server_python/tests/test_my_account_client.py +0 -160
  14. auth0_server_python-1.0.0b7/src/auth0_server_python/tests/test_server_client.py +0 -1934
  15. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/LICENSE +0 -0
  16. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
  17. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
  18. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/auth_server/__init__.py +0 -0
  19. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/encryption/__init__.py +0 -0
  20. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/encryption/encrypt.py +0 -0
  21. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/store/__init__.py +0 -0
  22. {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b9}/src/auth0_server_python/utils/__init__.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.0b9
4
4
  Summary: Auth0 server-side Python SDK
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.14
18
18
  Requires-Dist: authlib (>=1.2,<2.0)
19
19
  Requires-Dist: cryptography (>=43.0.1)
20
20
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
21
- Requires-Dist: jwcrypto (>=1.5.6,<2.0.0)
21
+ Requires-Dist: jwcrypto (>=1.5.7,<2.0.0)
22
22
  Requires-Dist: pydantic (>=2.10.6,<3.0.0)
23
23
  Requires-Dist: pyjwt (>=2.8.0)
24
24
  Description-Content-Type: text/markdown
@@ -129,6 +129,75 @@ 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
+
173
+ ### 5. Multiple Custom Domains (MCD)
174
+
175
+ For applications that use multiple custom domains on the same Auth0 tenant, pass a domain resolver function instead of a static domain string:
176
+
177
+ ```python
178
+ from auth0_server_python.auth_server.server_client import ServerClient
179
+ from auth0_server_python.auth_types import DomainResolverContext
180
+
181
+ async def domain_resolver(context: DomainResolverContext) -> str:
182
+ host = context.request_headers.get('host', '').split(':')[0]
183
+ domain_map = {
184
+ "acme.yourapp.com": "acme.auth0.com",
185
+ "globex.yourapp.com": "globex.auth0.com",
186
+ }
187
+ return domain_map.get(host, "default.auth0.com")
188
+
189
+ auth0 = ServerClient(
190
+ domain=domain_resolver, # Callable enables MCD mode
191
+ client_id='<AUTH0_CLIENT_ID>',
192
+ client_secret='<AUTH0_CLIENT_SECRET>',
193
+ secret='<AUTH0_SECRET>',
194
+ )
195
+ ```
196
+
197
+ The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and session isolation automatically. Static string domains continue to work unchanged.
198
+
199
+ For more details and examples, see [examples/MultipleCustomDomains.md](examples/MultipleCustomDomains.md).
200
+
132
201
  ## Feedback
133
202
 
134
203
  ### Contributing
@@ -104,6 +104,75 @@ 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
+
148
+ ### 5. Multiple Custom Domains (MCD)
149
+
150
+ For applications that use multiple custom domains on the same Auth0 tenant, pass a domain resolver function instead of a static domain string:
151
+
152
+ ```python
153
+ from auth0_server_python.auth_server.server_client import ServerClient
154
+ from auth0_server_python.auth_types import DomainResolverContext
155
+
156
+ async def domain_resolver(context: DomainResolverContext) -> str:
157
+ host = context.request_headers.get('host', '').split(':')[0]
158
+ domain_map = {
159
+ "acme.yourapp.com": "acme.auth0.com",
160
+ "globex.yourapp.com": "globex.auth0.com",
161
+ }
162
+ return domain_map.get(host, "default.auth0.com")
163
+
164
+ auth0 = ServerClient(
165
+ domain=domain_resolver, # Callable enables MCD mode
166
+ client_id='<AUTH0_CLIENT_ID>',
167
+ client_secret='<AUTH0_CLIENT_SECRET>',
168
+ secret='<AUTH0_SECRET>',
169
+ )
170
+ ```
171
+
172
+ The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and session isolation automatically. Static string domains continue to work unchanged.
173
+
174
+ For more details and examples, see [examples/MultipleCustomDomains.md](examples/MultipleCustomDomains.md).
175
+
107
176
  ## Feedback
108
177
 
109
178
  ### 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.0b9"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -17,7 +17,7 @@ pyjwt = ">=2.8.0"
17
17
  authlib = "^1.2"
18
18
  httpx = "^0.28.1"
19
19
  pydantic = "^2.10.6"
20
- jwcrypto = "^1.5.6"
20
+ jwcrypto = "^1.5.7"
21
21
 
22
22
  [tool.poetry.group.dev.dependencies]
23
23
  pytest = "^7.2"
@@ -25,7 +25,7 @@ pytest-cov = "^4.0"
25
25
  pytest-asyncio = ">=0.20.3,<0.24.0"
26
26
  pytest-mock = "^3.14.0"
27
27
  twine = "^6.1.0"
28
- ruff = "^0.1.0"
28
+ ruff = ">=0.1"
29
29
 
30
30
  [tool.pytest.ini_options]
31
31
  addopts = "--cov=auth0_server_python --cov-report=term-missing:skip-covered --cov-report=xml"
@@ -0,0 +1,335 @@
1
+
2
+ from typing import Optional
3
+
4
+ import httpx
5
+
6
+ from auth0_server_python.auth_schemes.bearer_auth import BearerAuth
7
+ from auth0_server_python.auth_types import (
8
+ CompleteConnectAccountRequest,
9
+ CompleteConnectAccountResponse,
10
+ ConnectAccountRequest,
11
+ ConnectAccountResponse,
12
+ ListConnectedAccountConnectionsResponse,
13
+ ListConnectedAccountsResponse,
14
+ )
15
+ from auth0_server_python.error import (
16
+ ApiError,
17
+ InvalidArgumentError,
18
+ MissingRequiredArgumentError,
19
+ MyAccountApiError,
20
+ )
21
+
22
+
23
+ class MyAccountClient:
24
+ """
25
+ Client for interacting with the Auth0 MyAccount API.
26
+ """
27
+
28
+ def __init__(self, domain: str):
29
+ """
30
+ Initialize the MyAccount API client.
31
+
32
+ Args:
33
+ domain: Auth0 domain (e.g., '<tenant>.<locality>.auth0.com')
34
+ """
35
+ self._domain = domain
36
+
37
+ @property
38
+ def audience(self):
39
+ """
40
+ Get the MyAccount API audience URL.
41
+
42
+ Returns:
43
+ The audience URL for the MyAccount API
44
+ """
45
+ return f"https://{self._domain}/me/"
46
+
47
+ async def connect_account(
48
+ self,
49
+ access_token: str,
50
+ request: ConnectAccountRequest
51
+ ) -> ConnectAccountResponse:
52
+ """
53
+ Initiate the connected account flow.
54
+
55
+ Args:
56
+ access_token: User's access token for authentication
57
+ request: Request containing connection details and configuration
58
+
59
+ Returns:
60
+ Response containing the connect URI and authentication session details
61
+
62
+ Raises:
63
+ MyAccountApiError: If the API returns an error response
64
+ ApiError: If the request fails due to network or other issues
65
+ """
66
+ try:
67
+ async with httpx.AsyncClient() as client:
68
+ response = await client.post(
69
+ url=f"{self.audience}v1/connected-accounts/connect",
70
+ json=request.model_dump(exclude_none=True),
71
+ auth=BearerAuth(access_token)
72
+ )
73
+
74
+ if response.status_code != 201:
75
+ error_data = response.json()
76
+ raise MyAccountApiError(
77
+ title=error_data.get("title", None),
78
+ type=error_data.get("type", None),
79
+ detail=error_data.get("detail", None),
80
+ status=error_data.get("status", None),
81
+ validation_errors=error_data.get("validation_errors", None)
82
+ )
83
+
84
+ data = response.json()
85
+
86
+ return ConnectAccountResponse.model_validate(data)
87
+
88
+ except Exception as e:
89
+ if isinstance(e, MyAccountApiError):
90
+ raise
91
+ raise ApiError(
92
+ "connect_account_error",
93
+ f"Connected Accounts connect request failed: {str(e) or 'Unknown error'}",
94
+ e
95
+ )
96
+
97
+ async def complete_connect_account(
98
+ self,
99
+ access_token: str,
100
+ request: CompleteConnectAccountRequest
101
+ ) -> CompleteConnectAccountResponse:
102
+ """
103
+ Complete the connected account flow after user authorization.
104
+
105
+ Args:
106
+ access_token: User's access token for authentication
107
+ request: Request containing the auth session, connect code, and redirect URI
108
+
109
+ Returns:
110
+ Response containing the connected account details including ID, connection, and scopes
111
+
112
+ Raises:
113
+ MyAccountApiError: If the API returns an error response
114
+ ApiError: If the request fails due to network or other issues
115
+ """
116
+ try:
117
+ async with httpx.AsyncClient() as client:
118
+ response = await client.post(
119
+ url=f"{self.audience}v1/connected-accounts/complete",
120
+ json=request.model_dump(exclude_none=True),
121
+ auth=BearerAuth(access_token)
122
+ )
123
+
124
+ if response.status_code != 201:
125
+ error_data = response.json()
126
+ raise MyAccountApiError(
127
+ title=error_data.get("title", None),
128
+ type=error_data.get("type", None),
129
+ detail=error_data.get("detail", None),
130
+ status=error_data.get("status", None),
131
+ validation_errors=error_data.get("validation_errors", None)
132
+ )
133
+
134
+ data = response.json()
135
+
136
+ return CompleteConnectAccountResponse.model_validate(data)
137
+
138
+ except Exception as e:
139
+ if isinstance(e, MyAccountApiError):
140
+ raise
141
+ raise ApiError(
142
+ "connect_account_error",
143
+ f"Connected Accounts complete request failed: {str(e) or 'Unknown error'}",
144
+ e
145
+ )
146
+
147
+ async def list_connected_accounts(
148
+ self,
149
+ access_token: str,
150
+ connection: Optional[str] = None,
151
+ from_param: Optional[str] = None,
152
+ take: Optional[int] = None
153
+ ) -> ListConnectedAccountsResponse:
154
+ """
155
+ List connected accounts for the authenticated user.
156
+
157
+ Args:
158
+ access_token: User's access token for authentication
159
+ connection: Optional filter to list accounts for a specific connection
160
+ from_param: Optional pagination cursor for fetching next page of results
161
+ take: Optional number of results to return (must be a positive integer)
162
+
163
+ Returns:
164
+ Response containing the list of connected accounts and pagination details
165
+
166
+ Raises:
167
+ MissingRequiredArgumentError: If access_token is not provided
168
+ InvalidArgumentError: If take parameter is not a positive integer
169
+ MyAccountApiError: If the API returns an error response
170
+ ApiError: If the request fails due to network or other issues
171
+ """
172
+ if access_token is None:
173
+ raise MissingRequiredArgumentError("access_token")
174
+
175
+ if take is not None and (not isinstance(take, int) or take < 1):
176
+ raise InvalidArgumentError("take", "The 'take' parameter must be a positive integer.")
177
+
178
+ try:
179
+ async with httpx.AsyncClient() as client:
180
+ params = {}
181
+ if connection:
182
+ params["connection"] = connection
183
+ if from_param:
184
+ params["from"] = from_param
185
+ if take:
186
+ params["take"] = take
187
+
188
+ response = await client.get(
189
+ url=f"{self.audience}v1/connected-accounts/accounts",
190
+ params=params,
191
+ auth=BearerAuth(access_token)
192
+ )
193
+
194
+ if response.status_code != 200:
195
+ error_data = response.json()
196
+ raise MyAccountApiError(
197
+ title=error_data.get("title", None),
198
+ type=error_data.get("type", None),
199
+ detail=error_data.get("detail", None),
200
+ status=error_data.get("status", None),
201
+ validation_errors=error_data.get("validation_errors", None)
202
+ )
203
+
204
+ data = response.json()
205
+
206
+ return ListConnectedAccountsResponse.model_validate(data)
207
+
208
+ except Exception as e:
209
+ if isinstance(e, MyAccountApiError):
210
+ raise
211
+ raise ApiError(
212
+ "connect_account_error",
213
+ f"Connected Accounts list request failed: {str(e) or 'Unknown error'}",
214
+ e
215
+ )
216
+
217
+
218
+ async def delete_connected_account(
219
+ self,
220
+ access_token: str,
221
+ connected_account_id: str
222
+ ) -> None:
223
+ """
224
+ Delete a connected account for the authenticated user.
225
+
226
+ Args:
227
+ access_token: User's access token for authentication
228
+ connected_account_id: ID of the connected account to delete
229
+
230
+ Returns:
231
+ None
232
+
233
+ Raises:
234
+ MissingRequiredArgumentError: If access_token or connected_account_id is not provided
235
+ MyAccountApiError: If the API returns an error response
236
+ ApiError: If the request fails due to network or other issues
237
+ """
238
+
239
+ if access_token is None:
240
+ raise MissingRequiredArgumentError("access_token")
241
+
242
+ if connected_account_id is None:
243
+ raise MissingRequiredArgumentError("connected_account_id")
244
+
245
+ try:
246
+ async with httpx.AsyncClient() as client:
247
+ response = await client.delete(
248
+ url=f"{self.audience}v1/connected-accounts/accounts/{connected_account_id}",
249
+ auth=BearerAuth(access_token)
250
+ )
251
+
252
+ if response.status_code != 204:
253
+ error_data = response.json()
254
+ raise MyAccountApiError(
255
+ title=error_data.get("title", None),
256
+ type=error_data.get("type", None),
257
+ detail=error_data.get("detail", None),
258
+ status=error_data.get("status", None),
259
+ validation_errors=error_data.get("validation_errors", None)
260
+ )
261
+
262
+ except Exception as e:
263
+ if isinstance(e, MyAccountApiError):
264
+ raise
265
+ raise ApiError(
266
+ "connect_account_error",
267
+ f"Connected Accounts delete request failed: {str(e) or 'Unknown error'}",
268
+ e
269
+ )
270
+
271
+ async def list_connected_account_connections(
272
+ self,
273
+ access_token: str,
274
+ from_param: Optional[str] = None,
275
+ take: Optional[int] = None
276
+ ) -> ListConnectedAccountConnectionsResponse:
277
+ """
278
+ List available connections that support connected accounts.
279
+
280
+ Args:
281
+ access_token: User's access token for authentication
282
+ from_param: Optional pagination cursor for fetching next page of results
283
+ take: Optional number of results to return (must be a positive integer)
284
+
285
+ Returns:
286
+ Response containing the list of available connections and pagination details
287
+
288
+ Raises:
289
+ MissingRequiredArgumentError: If access_token is not provided
290
+ InvalidArgumentError: If take parameter is not a positive integer
291
+ MyAccountApiError: If the API returns an error response
292
+ ApiError: If the request fails due to network or other issues
293
+ """
294
+ if access_token is None:
295
+ raise MissingRequiredArgumentError("access_token")
296
+
297
+ if take is not None and (not isinstance(take, int) or take < 1):
298
+ raise InvalidArgumentError("take", "The 'take' parameter must be a positive integer.")
299
+
300
+ try:
301
+ async with httpx.AsyncClient() as client:
302
+ params = {}
303
+ if from_param:
304
+ params["from"] = from_param
305
+ if take:
306
+ params["take"] = take
307
+
308
+ response = await client.get(
309
+ url=f"{self.audience}v1/connected-accounts/connections",
310
+ params=params,
311
+ auth=BearerAuth(access_token)
312
+ )
313
+
314
+ if response.status_code != 200:
315
+ error_data = response.json()
316
+ raise MyAccountApiError(
317
+ title=error_data.get("title", None),
318
+ type=error_data.get("type", None),
319
+ detail=error_data.get("detail", None),
320
+ status=error_data.get("status", None),
321
+ validation_errors=error_data.get("validation_errors", None)
322
+ )
323
+
324
+ data = response.json()
325
+
326
+ return ListConnectedAccountConnectionsResponse.model_validate(data)
327
+
328
+ except Exception as e:
329
+ if isinstance(e, MyAccountApiError):
330
+ raise
331
+ raise ApiError(
332
+ "connect_account_error",
333
+ f"Connected Accounts list connections request failed: {str(e) or 'Unknown error'}",
334
+ e
335
+ )