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.
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/PKG-INFO +42 -1
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/README.md +41 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/pyproject.toml +1 -1
- auth0_server_python-1.0.0b8/src/auth0_server_python/auth_server/my_account_client.py +334 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_server/server_client.py +834 -449
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_types/__init__.py +156 -10
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/error/__init__.py +31 -0
- auth0_server_python-1.0.0b8/src/auth0_server_python/tests/test_my_account_client.py +503 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/tests/test_server_client.py +892 -0
- auth0_server_python-1.0.0b7/src/auth0_server_python/auth_server/my_account_client.py +0 -94
- auth0_server_python-1.0.0b7/src/auth0_server_python/tests/test_my_account_client.py +0 -160
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/LICENSE +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/auth_server/__init__.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/encryption/__init__.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/encryption/encrypt.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/store/__init__.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/store/abstract.py +0 -0
- {auth0_server_python-1.0.0b7 → auth0_server_python-1.0.0b8}/src/auth0_server_python/utils/__init__.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
+
)
|