auth0-server-python 1.0.0b3__tar.gz → 1.0.0b4__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.0b3 → auth0_server_python-1.0.0b4}/PKG-INFO +1 -1
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/pyproject.toml +1 -1
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/auth_server/server_client.py +271 -239
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/LICENSE +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/README.md +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/auth_server/__init__.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/auth_types/__init__.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/encryption/__init__.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/encryption/encrypt.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/error/__init__.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/store/__init__.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/store/abstract.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/utils/__init__.py +0 -0
- {auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/utils/helpers.py +0 -0
|
@@ -12,13 +12,13 @@ import jwt
|
|
|
12
12
|
|
|
13
13
|
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
14
14
|
from authlib.integrations.base_client.errors import OAuthError
|
|
15
|
-
import httpx
|
|
15
|
+
import httpx
|
|
16
16
|
|
|
17
17
|
from pydantic import BaseModel, ValidationError
|
|
18
18
|
|
|
19
19
|
from auth0_server_python.error import (
|
|
20
|
-
MissingTransactionError,
|
|
21
|
-
ApiError,
|
|
20
|
+
MissingTransactionError,
|
|
21
|
+
ApiError,
|
|
22
22
|
MissingRequiredArgumentError,
|
|
23
23
|
BackchannelLogoutError,
|
|
24
24
|
AccessTokenError,
|
|
@@ -26,12 +26,12 @@ from auth0_server_python.error import (
|
|
|
26
26
|
StartLinkUserError,
|
|
27
27
|
AccessTokenErrorCode,
|
|
28
28
|
AccessTokenForConnectionErrorCode
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
)
|
|
31
31
|
from auth0_server_python.auth_types import (
|
|
32
|
-
StateData,
|
|
33
|
-
TransactionData,
|
|
34
|
-
UserClaims,
|
|
32
|
+
StateData,
|
|
33
|
+
TransactionData,
|
|
34
|
+
UserClaims,
|
|
35
35
|
TokenSet,
|
|
36
36
|
LogoutTokenClaims,
|
|
37
37
|
StartInteractiveLoginOptions,
|
|
@@ -42,13 +42,16 @@ from auth0_server_python.utils import PKCE, State, URL
|
|
|
42
42
|
|
|
43
43
|
# Generic type for store options
|
|
44
44
|
TStoreOptions = TypeVar('TStoreOptions')
|
|
45
|
+
INTERNAL_AUTHORIZE_PARAMS = ["client_id", "redirect_uri", "response_type",
|
|
46
|
+
"code_challenge", "code_challenge_method", "state", "nonce"]
|
|
47
|
+
|
|
45
48
|
|
|
46
49
|
class ServerClient(Generic[TStoreOptions]):
|
|
47
50
|
"""
|
|
48
51
|
Main client for Auth0 server SDK. Handles authentication flows, session management,
|
|
49
52
|
and token operations using Authlib for OIDC functionality.
|
|
50
53
|
"""
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
def __init__(
|
|
53
56
|
self,
|
|
54
57
|
domain: str,
|
|
@@ -56,8 +59,8 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
56
59
|
client_secret: str,
|
|
57
60
|
redirect_uri: Optional[str] = None,
|
|
58
61
|
secret: str = None,
|
|
59
|
-
transaction_store
|
|
60
|
-
state_store
|
|
62
|
+
transaction_store=None,
|
|
63
|
+
state_store=None,
|
|
61
64
|
transaction_identifier: str = "_a0_tx",
|
|
62
65
|
state_identifier: str = "_a0_session",
|
|
63
66
|
authorization_params: Optional[Dict[str, Any]] = None,
|
|
@@ -65,7 +68,7 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
65
68
|
):
|
|
66
69
|
"""
|
|
67
70
|
Initialize the Auth0 server client.
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
Args:
|
|
70
73
|
domain: Auth0 domain (e.g., 'your-tenant.auth0.com')
|
|
71
74
|
client_id: Auth0 client ID
|
|
@@ -80,7 +83,7 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
80
83
|
"""
|
|
81
84
|
if not secret:
|
|
82
85
|
raise MissingRequiredArgumentError("secret")
|
|
83
|
-
|
|
86
|
+
|
|
84
87
|
# Store configuration
|
|
85
88
|
self._domain = domain
|
|
86
89
|
self._client_id = client_id
|
|
@@ -88,13 +91,13 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
88
91
|
self._redirect_uri = redirect_uri
|
|
89
92
|
self._default_authorization_params = authorization_params or {}
|
|
90
93
|
self._pushed_authorization_requests = pushed_authorization_requests # store the flag
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
# Initialize stores
|
|
93
96
|
self._transaction_store = transaction_store
|
|
94
97
|
self._state_store = state_store
|
|
95
98
|
self._transaction_identifier = transaction_identifier
|
|
96
99
|
self._state_identifier = state_identifier
|
|
97
|
-
|
|
100
|
+
|
|
98
101
|
# Initialize OAuth client
|
|
99
102
|
self._oauth = AsyncOAuth2Client(
|
|
100
103
|
client_id=client_id,
|
|
@@ -108,7 +111,6 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
108
111
|
response.raise_for_status()
|
|
109
112
|
return response.json()
|
|
110
113
|
|
|
111
|
-
|
|
112
114
|
async def start_interactive_login(
|
|
113
115
|
self,
|
|
114
116
|
options: Optional[StartInteractiveLoginOptions] = None,
|
|
@@ -116,40 +118,43 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
116
118
|
) -> str:
|
|
117
119
|
"""
|
|
118
120
|
Starts the interactive login process and returns a URL to redirect to.
|
|
119
|
-
|
|
121
|
+
|
|
120
122
|
Args:
|
|
121
123
|
options: Configuration options for the login process
|
|
122
|
-
|
|
124
|
+
|
|
123
125
|
Returns:
|
|
124
126
|
Authorization URL to redirect the user to
|
|
125
127
|
"""
|
|
126
128
|
options = options or StartInteractiveLoginOptions()
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
# Get effective authorization params (merge defaults with provided ones)
|
|
129
131
|
auth_params = dict(self._default_authorization_params)
|
|
130
132
|
if options.authorization_params:
|
|
131
|
-
auth_params.update(
|
|
132
|
-
|
|
133
|
+
auth_params.update(
|
|
134
|
+
{k: v for k, v in options.authorization_params.items(
|
|
135
|
+
) if k not in INTERNAL_AUTHORIZE_PARAMS}
|
|
136
|
+
)
|
|
137
|
+
|
|
133
138
|
# Ensure we have a redirect_uri
|
|
134
139
|
if "redirect_uri" not in auth_params and not self._redirect_uri:
|
|
135
140
|
raise MissingRequiredArgumentError("redirect_uri")
|
|
136
|
-
|
|
141
|
+
|
|
137
142
|
# Use the default redirect_uri if none is specified
|
|
138
143
|
if "redirect_uri" not in auth_params and self._redirect_uri:
|
|
139
144
|
auth_params["redirect_uri"] = self._redirect_uri
|
|
140
|
-
|
|
145
|
+
|
|
141
146
|
# Generate PKCE code verifier and challenge
|
|
142
147
|
code_verifier = PKCE.generate_code_verifier()
|
|
143
148
|
code_challenge = PKCE.generate_code_challenge(code_verifier)
|
|
144
|
-
|
|
149
|
+
|
|
145
150
|
# Add PKCE parameters to the authorization request
|
|
146
151
|
auth_params["code_challenge"] = code_challenge
|
|
147
152
|
auth_params["code_challenge_method"] = "S256"
|
|
148
|
-
|
|
153
|
+
|
|
149
154
|
# State parameter to prevent CSRF
|
|
150
155
|
state = PKCE.generate_random_string(32)
|
|
151
156
|
auth_params["state"] = state
|
|
152
|
-
|
|
157
|
+
|
|
153
158
|
# Build the transaction data to store
|
|
154
159
|
transaction_data = TransactionData(
|
|
155
160
|
code_verifier=code_verifier,
|
|
@@ -158,20 +163,23 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
158
163
|
|
|
159
164
|
# Store the transaction data
|
|
160
165
|
await self._transaction_store.set(
|
|
161
|
-
f"{self._transaction_identifier}:{state}",
|
|
166
|
+
f"{self._transaction_identifier}:{state}",
|
|
162
167
|
transaction_data,
|
|
163
168
|
options=store_options
|
|
164
169
|
)
|
|
165
170
|
try:
|
|
166
171
|
self._oauth.metadata = await self._fetch_oidc_metadata(self._domain)
|
|
167
172
|
except Exception as e:
|
|
168
|
-
raise ApiError("metadata_error",
|
|
173
|
+
raise ApiError("metadata_error",
|
|
174
|
+
"Failed to fetch OIDC metadata", e)
|
|
169
175
|
# If PAR is enabled, use the PAR endpoint
|
|
170
176
|
if self._pushed_authorization_requests:
|
|
171
|
-
par_endpoint = self._oauth.metadata.get(
|
|
177
|
+
par_endpoint = self._oauth.metadata.get(
|
|
178
|
+
"pushed_authorization_request_endpoint")
|
|
172
179
|
if not par_endpoint:
|
|
173
|
-
raise ApiError(
|
|
174
|
-
|
|
180
|
+
raise ApiError(
|
|
181
|
+
"configuration_error", "PAR is enabled but pushed_authorization_request_endpoint is missing in metadata")
|
|
182
|
+
|
|
175
183
|
auth_params["client_id"] = self._client_id
|
|
176
184
|
# Post the auth_params to the PAR endpoint
|
|
177
185
|
async with httpx.AsyncClient() as client:
|
|
@@ -184,71 +192,76 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
184
192
|
error_data = par_response.json()
|
|
185
193
|
raise ApiError(
|
|
186
194
|
error_data.get("error", "par_error"),
|
|
187
|
-
error_data.get(
|
|
195
|
+
error_data.get(
|
|
196
|
+
"error_description", "Failed to obtain request_uri from PAR endpoint")
|
|
188
197
|
)
|
|
189
198
|
par_data = par_response.json()
|
|
190
199
|
request_uri = par_data.get("request_uri")
|
|
191
200
|
if not request_uri:
|
|
192
|
-
raise ApiError(
|
|
193
|
-
|
|
201
|
+
raise ApiError(
|
|
202
|
+
"par_error", "No request_uri returned from PAR endpoint")
|
|
203
|
+
|
|
194
204
|
auth_endpoint = self._oauth.metadata.get("authorization_endpoint")
|
|
195
205
|
final_url = f"{auth_endpoint}?request_uri={request_uri}&response_type={auth_params['response_type']}&client_id={self._client_id}"
|
|
196
206
|
return final_url
|
|
197
207
|
else:
|
|
198
208
|
if "authorization_endpoint" not in self._oauth.metadata:
|
|
199
|
-
raise ApiError("configuration_error",
|
|
209
|
+
raise ApiError("configuration_error",
|
|
210
|
+
"Authorization endpoint missing in OIDC metadata")
|
|
200
211
|
|
|
201
212
|
authorization_endpoint = self._oauth.metadata["authorization_endpoint"]
|
|
202
213
|
|
|
203
214
|
try:
|
|
204
|
-
auth_url, state = self._oauth.create_authorization_url(
|
|
215
|
+
auth_url, state = self._oauth.create_authorization_url(
|
|
216
|
+
authorization_endpoint, **auth_params)
|
|
205
217
|
except Exception as e:
|
|
206
|
-
raise ApiError("authorization_url_error",
|
|
218
|
+
raise ApiError("authorization_url_error",
|
|
219
|
+
"Failed to create authorization URL", e)
|
|
207
220
|
|
|
208
221
|
return auth_url
|
|
209
|
-
|
|
222
|
+
|
|
210
223
|
async def complete_interactive_login(
|
|
211
|
-
self,
|
|
224
|
+
self,
|
|
212
225
|
url: str,
|
|
213
226
|
store_options: dict = None
|
|
214
227
|
) -> Dict[str, Any]:
|
|
215
228
|
"""
|
|
216
229
|
Completes the login process after user is redirected back.
|
|
217
|
-
|
|
230
|
+
|
|
218
231
|
Args:
|
|
219
232
|
url: The full callback URL including query parameters
|
|
220
233
|
store_options: Options to pass to the state store
|
|
221
|
-
|
|
234
|
+
|
|
222
235
|
Returns:
|
|
223
236
|
Dictionary containing session data and app state
|
|
224
237
|
"""
|
|
225
238
|
# Parse the URL to get query parameters
|
|
226
239
|
parsed_url = urlparse(url)
|
|
227
240
|
query_params = parse_qs(parsed_url.query)
|
|
228
|
-
|
|
241
|
+
|
|
229
242
|
# Get state parameter from the URL
|
|
230
243
|
state = query_params.get("state", [""])[0]
|
|
231
244
|
if not state:
|
|
232
245
|
raise MissingRequiredArgumentError("state")
|
|
233
|
-
|
|
246
|
+
|
|
234
247
|
# Retrieve the transaction data using the state
|
|
235
248
|
transaction_identifier = f"{self._transaction_identifier}:{state}"
|
|
236
249
|
transaction_data = await self._transaction_store.get(transaction_identifier, options=store_options)
|
|
237
|
-
|
|
250
|
+
|
|
238
251
|
if not transaction_data:
|
|
239
252
|
raise MissingTransactionError()
|
|
240
|
-
|
|
253
|
+
|
|
241
254
|
# Check for error response from Auth0
|
|
242
255
|
if "error" in query_params:
|
|
243
256
|
error = query_params.get("error", [""])[0]
|
|
244
257
|
error_description = query_params.get("error_description", [""])[0]
|
|
245
258
|
raise ApiError(error, error_description)
|
|
246
|
-
|
|
259
|
+
|
|
247
260
|
# Get the authorization code from the URL
|
|
248
261
|
code = query_params.get("code", [""])[0]
|
|
249
262
|
if not code:
|
|
250
263
|
raise MissingRequiredArgumentError("code")
|
|
251
|
-
|
|
264
|
+
|
|
252
265
|
if not self._oauth.metadata or "token_endpoint" not in self._oauth.metadata:
|
|
253
266
|
self._oauth.metadata = await self._fetch_oidc_metadata(self._domain)
|
|
254
267
|
|
|
@@ -263,57 +276,62 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
263
276
|
)
|
|
264
277
|
except OAuthError as e:
|
|
265
278
|
# Raise a custom error (or handle it as appropriate)
|
|
266
|
-
raise ApiError(
|
|
267
|
-
|
|
279
|
+
raise ApiError(
|
|
280
|
+
"token_error", f"Token exchange failed: {str(e)}", e)
|
|
281
|
+
|
|
268
282
|
# Use the userinfo field from the token_response for user claims
|
|
269
283
|
user_info = token_response.get("userinfo")
|
|
270
284
|
user_claims = None
|
|
271
285
|
if user_info:
|
|
272
286
|
user_claims = UserClaims.parse_obj(user_info)
|
|
273
287
|
else:
|
|
274
|
-
id_token = token_response.get("id_token")
|
|
275
|
-
if id_token:
|
|
276
|
-
claims = jwt.decode(id_token, options={
|
|
288
|
+
id_token = token_response.get("id_token")
|
|
289
|
+
if id_token:
|
|
290
|
+
claims = jwt.decode(id_token, options={
|
|
291
|
+
"verify_signature": False})
|
|
277
292
|
user_claims = UserClaims.parse_obj(claims)
|
|
278
|
-
|
|
293
|
+
|
|
279
294
|
# Build a token set using the token response data
|
|
280
295
|
token_set = TokenSet(
|
|
281
296
|
audience=token_response.get("audience", "default"),
|
|
282
297
|
access_token=token_response.get("access_token", ""),
|
|
283
298
|
scope=token_response.get("scope", ""),
|
|
284
|
-
expires_at=int(time.time()) +
|
|
299
|
+
expires_at=int(time.time()) +
|
|
300
|
+
token_response.get("expires_in", 3600)
|
|
285
301
|
)
|
|
286
|
-
|
|
302
|
+
|
|
287
303
|
# Generate a session id (sid) from token_response or transaction data, or create a new one
|
|
288
|
-
sid = user_info.get(
|
|
289
|
-
|
|
304
|
+
sid = user_info.get(
|
|
305
|
+
"sid") if user_info and "sid" in user_info else PKCE.generate_random_string(32)
|
|
306
|
+
|
|
290
307
|
# Construct state data to represent the session
|
|
291
308
|
state_data = StateData(
|
|
292
309
|
user=user_claims,
|
|
293
310
|
id_token=token_response.get("id_token"),
|
|
294
|
-
|
|
311
|
+
# might be None if not provided
|
|
312
|
+
refresh_token=token_response.get("refresh_token"),
|
|
295
313
|
token_sets=[token_set],
|
|
296
314
|
internal={
|
|
297
315
|
"sid": sid,
|
|
298
316
|
"created_at": int(time.time())
|
|
299
317
|
}
|
|
300
318
|
)
|
|
301
|
-
|
|
319
|
+
|
|
302
320
|
# Store the state data in the state store using store_options (Response required)
|
|
303
321
|
await self._state_store.set(self._state_identifier, state_data, options=store_options)
|
|
304
|
-
|
|
322
|
+
|
|
305
323
|
# Clean up transaction data after successful login
|
|
306
324
|
await self._transaction_store.delete(transaction_identifier, options=store_options)
|
|
307
|
-
|
|
325
|
+
|
|
308
326
|
result = {"state_data": state_data.dict()}
|
|
309
327
|
if transaction_data.app_state:
|
|
310
328
|
result["app_state"] = transaction_data.app_state
|
|
311
|
-
|
|
312
|
-
#For RAR
|
|
329
|
+
|
|
330
|
+
# For RAR
|
|
313
331
|
authorization_details = token_response.get("authorization_details")
|
|
314
332
|
if authorization_details:
|
|
315
333
|
result["authorization_details"] = authorization_details
|
|
316
|
-
|
|
334
|
+
|
|
317
335
|
return result
|
|
318
336
|
|
|
319
337
|
async def start_link_user(
|
|
@@ -323,25 +341,25 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
323
341
|
):
|
|
324
342
|
"""
|
|
325
343
|
Starts the user linking process, and returns a URL to redirect the user-agent to.
|
|
326
|
-
|
|
344
|
+
|
|
327
345
|
Args:
|
|
328
346
|
options: Options used to configure the user linking process.
|
|
329
347
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
330
|
-
|
|
348
|
+
|
|
331
349
|
Returns:
|
|
332
350
|
URL to redirect the user to for authentication.
|
|
333
351
|
"""
|
|
334
352
|
state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
335
|
-
|
|
353
|
+
|
|
336
354
|
if not state_data or not state_data.get("id_token"):
|
|
337
355
|
raise StartLinkUserError(
|
|
338
356
|
"Unable to start the user linking process without a logged in user. Ensure to login using the SDK before starting the user linking process."
|
|
339
357
|
)
|
|
340
|
-
|
|
358
|
+
|
|
341
359
|
# Generate PKCE and state for security
|
|
342
360
|
code_verifier = PKCE.generate_code_verifier()
|
|
343
361
|
state = PKCE.generate_random_string(32)
|
|
344
|
-
|
|
362
|
+
|
|
345
363
|
# Build the URL for user linking
|
|
346
364
|
link_user_url = await self._build_link_user_url(
|
|
347
365
|
connection=options.get("connection"),
|
|
@@ -351,21 +369,21 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
351
369
|
state=state,
|
|
352
370
|
authorization_params=options.get("authorization_params")
|
|
353
371
|
)
|
|
354
|
-
|
|
372
|
+
|
|
355
373
|
# Store transaction data
|
|
356
374
|
transaction_data = TransactionData(
|
|
357
375
|
code_verifier=code_verifier,
|
|
358
376
|
app_state=options.get("app_state")
|
|
359
377
|
)
|
|
360
|
-
|
|
378
|
+
|
|
361
379
|
await self._transaction_store.set(
|
|
362
|
-
f"{self._transaction_identifier}:{state}",
|
|
380
|
+
f"{self._transaction_identifier}:{state}",
|
|
363
381
|
transaction_data,
|
|
364
382
|
options=store_options
|
|
365
383
|
)
|
|
366
|
-
|
|
384
|
+
|
|
367
385
|
return link_user_url
|
|
368
|
-
|
|
386
|
+
|
|
369
387
|
async def complete_link_user(
|
|
370
388
|
self,
|
|
371
389
|
url: str,
|
|
@@ -373,23 +391,23 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
373
391
|
) -> Dict[str, Any]:
|
|
374
392
|
"""
|
|
375
393
|
Completes the user linking process.
|
|
376
|
-
|
|
394
|
+
|
|
377
395
|
Args:
|
|
378
396
|
url: The URL from which the query params should be extracted
|
|
379
397
|
store_options: Optional options for the stores
|
|
380
|
-
|
|
398
|
+
|
|
381
399
|
Returns:
|
|
382
400
|
Dictionary containing the original app state
|
|
383
401
|
"""
|
|
384
402
|
|
|
385
403
|
# We can reuse the interactive login completion since the flow is similar
|
|
386
404
|
result = await self.complete_interactive_login(url, store_options)
|
|
387
|
-
|
|
405
|
+
|
|
388
406
|
# Return just the app state as specified
|
|
389
407
|
return {
|
|
390
408
|
"app_state": result.get("app_state")
|
|
391
409
|
}
|
|
392
|
-
|
|
410
|
+
|
|
393
411
|
async def start_unlink_user(
|
|
394
412
|
self,
|
|
395
413
|
options,
|
|
@@ -397,25 +415,25 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
397
415
|
):
|
|
398
416
|
"""
|
|
399
417
|
Starts the user unlinking process, and returns a URL to redirect the user-agent to.
|
|
400
|
-
|
|
418
|
+
|
|
401
419
|
Args:
|
|
402
420
|
options: Options used to configure the user unlinking process.
|
|
403
421
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
404
|
-
|
|
422
|
+
|
|
405
423
|
Returns:
|
|
406
424
|
URL to redirect the user to for authentication.
|
|
407
425
|
"""
|
|
408
426
|
state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
409
|
-
|
|
427
|
+
|
|
410
428
|
if not state_data or not state_data.get("id_token"):
|
|
411
429
|
raise StartLinkUserError(
|
|
412
430
|
"Unable to start the user linking process without a logged in user. Ensure to login using the SDK before starting the user linking process."
|
|
413
431
|
)
|
|
414
|
-
|
|
432
|
+
|
|
415
433
|
# Generate PKCE and state for security
|
|
416
434
|
code_verifier = PKCE.generate_code_verifier()
|
|
417
435
|
state = PKCE.generate_random_string(32)
|
|
418
|
-
|
|
436
|
+
|
|
419
437
|
# Build the URL for user linking
|
|
420
438
|
link_user_url = await self._build_unlink_user_url(
|
|
421
439
|
connection=options.get("connection"),
|
|
@@ -424,21 +442,21 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
424
442
|
state=state,
|
|
425
443
|
authorization_params=options.get("authorization_params")
|
|
426
444
|
)
|
|
427
|
-
|
|
445
|
+
|
|
428
446
|
# Store transaction data
|
|
429
447
|
transaction_data = TransactionData(
|
|
430
448
|
code_verifier=code_verifier,
|
|
431
449
|
app_state=options.get("app_state")
|
|
432
450
|
)
|
|
433
|
-
|
|
451
|
+
|
|
434
452
|
await self._transaction_store.set(
|
|
435
|
-
f"{self._transaction_identifier}:{state}",
|
|
453
|
+
f"{self._transaction_identifier}:{state}",
|
|
436
454
|
transaction_data,
|
|
437
455
|
options=store_options
|
|
438
456
|
)
|
|
439
|
-
|
|
457
|
+
|
|
440
458
|
return link_user_url
|
|
441
|
-
|
|
459
|
+
|
|
442
460
|
async def complete_unlink_user(
|
|
443
461
|
self,
|
|
444
462
|
url: str,
|
|
@@ -446,25 +464,23 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
446
464
|
) -> Dict[str, Any]:
|
|
447
465
|
"""
|
|
448
466
|
Completes the user unlinking process.
|
|
449
|
-
|
|
467
|
+
|
|
450
468
|
Args:
|
|
451
469
|
url: The URL from which the query params should be extracted
|
|
452
470
|
store_options: Optional options for the stores
|
|
453
|
-
|
|
471
|
+
|
|
454
472
|
Returns:
|
|
455
473
|
Dictionary containing the original app state
|
|
456
474
|
"""
|
|
457
475
|
|
|
458
476
|
# We can reuse the interactive login completion since the flow is similar
|
|
459
477
|
result = await self.complete_interactive_login(url, store_options)
|
|
460
|
-
|
|
478
|
+
|
|
461
479
|
# Return just the app state as specified
|
|
462
480
|
return {
|
|
463
481
|
"app_state": result.get("app_state")
|
|
464
482
|
}
|
|
465
|
-
|
|
466
483
|
|
|
467
|
-
|
|
468
484
|
async def login_backchannel(
|
|
469
485
|
self,
|
|
470
486
|
options: Dict[str, Any],
|
|
@@ -472,18 +488,18 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
472
488
|
) -> Dict[str, Any]:
|
|
473
489
|
"""
|
|
474
490
|
Logs in using Client-Initiated Backchannel Authentication.
|
|
475
|
-
|
|
491
|
+
|
|
476
492
|
Note:
|
|
477
|
-
Using Client-Initiated Backchannel Authentication requires the feature
|
|
493
|
+
Using Client-Initiated Backchannel Authentication requires the feature
|
|
478
494
|
to be enabled in the Auth0 dashboard.
|
|
479
|
-
|
|
495
|
+
|
|
480
496
|
See:
|
|
481
497
|
https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow
|
|
482
|
-
|
|
498
|
+
|
|
483
499
|
Args:
|
|
484
500
|
options: Options used to configure the backchannel login process.
|
|
485
501
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
486
|
-
|
|
502
|
+
|
|
487
503
|
Returns:
|
|
488
504
|
A dictionary containing the authorizationDetails (when RAR was used).
|
|
489
505
|
"""
|
|
@@ -492,10 +508,11 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
492
508
|
"login_hint": options.get("login_hint"),
|
|
493
509
|
"authorization_params": options.get("authorization_params"),
|
|
494
510
|
})
|
|
495
|
-
|
|
511
|
+
|
|
496
512
|
existing_state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
497
|
-
|
|
498
|
-
audience = self._default_authorization_params.get(
|
|
513
|
+
|
|
514
|
+
audience = self._default_authorization_params.get(
|
|
515
|
+
"audience", "default")
|
|
499
516
|
|
|
500
517
|
state_data = State.update_state_data(
|
|
501
518
|
audience,
|
|
@@ -513,15 +530,15 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
513
530
|
async def get_user(self, store_options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
|
514
531
|
"""
|
|
515
532
|
Retrieves the user from the store, or None if no user found.
|
|
516
|
-
|
|
533
|
+
|
|
517
534
|
Args:
|
|
518
535
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
519
|
-
|
|
536
|
+
|
|
520
537
|
Returns:
|
|
521
538
|
The user, or None if no user found in the store.
|
|
522
539
|
"""
|
|
523
540
|
state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
524
|
-
|
|
541
|
+
|
|
525
542
|
if state_data:
|
|
526
543
|
if hasattr(state_data, "dict") and callable(state_data.dict):
|
|
527
544
|
state_data = state_data.dict()
|
|
@@ -531,45 +548,45 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
531
548
|
async def get_session(self, store_options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
|
532
549
|
"""
|
|
533
550
|
Retrieve the user session from the store, or None if no session found.
|
|
534
|
-
|
|
551
|
+
|
|
535
552
|
Args:
|
|
536
553
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
537
|
-
|
|
554
|
+
|
|
538
555
|
Returns:
|
|
539
556
|
The session, or None if no session found in the store.
|
|
540
557
|
"""
|
|
541
558
|
state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
542
|
-
|
|
559
|
+
|
|
543
560
|
if state_data:
|
|
544
561
|
if hasattr(state_data, "dict") and callable(state_data.dict):
|
|
545
562
|
state_data = state_data.dict()
|
|
546
|
-
session_data = {k: v for k, v in state_data.items()
|
|
563
|
+
session_data = {k: v for k, v in state_data.items()
|
|
564
|
+
if k != "internal"}
|
|
547
565
|
return session_data
|
|
548
566
|
return None
|
|
549
567
|
|
|
550
568
|
async def get_access_token(self, store_options: Optional[Dict[str, Any]] = None) -> str:
|
|
551
569
|
"""
|
|
552
|
-
Retrieves the access token from the store, or calls Auth0 when the access token
|
|
570
|
+
Retrieves the access token from the store, or calls Auth0 when the access token
|
|
553
571
|
is expired and a refresh token is available in the store.
|
|
554
572
|
Also updates the store when a new token was retrieved from Auth0.
|
|
555
|
-
|
|
573
|
+
|
|
556
574
|
Args:
|
|
557
575
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
558
|
-
|
|
576
|
+
|
|
559
577
|
Returns:
|
|
560
578
|
The access token, retrieved from the store or Auth0.
|
|
561
|
-
|
|
579
|
+
|
|
562
580
|
Raises:
|
|
563
581
|
AccessTokenError: If the token is expired and no refresh token is available.
|
|
564
582
|
"""
|
|
565
583
|
state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
566
|
-
|
|
584
|
+
|
|
567
585
|
# Get audience and scope from options or use defaults
|
|
568
586
|
auth_params = self._default_authorization_params or {}
|
|
569
587
|
audience = auth_params.get("audience", "default")
|
|
570
588
|
scope = auth_params.get("scope")
|
|
571
589
|
|
|
572
|
-
|
|
573
590
|
if state_data and hasattr(state_data, "dict") and callable(state_data.dict):
|
|
574
591
|
state_data_dict = state_data.dict()
|
|
575
592
|
else:
|
|
@@ -582,31 +599,32 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
582
599
|
if ts.get("audience") == audience and (not scope or ts.get("scope") == scope):
|
|
583
600
|
token_set = ts
|
|
584
601
|
break
|
|
585
|
-
|
|
602
|
+
|
|
586
603
|
# If token is valid, return it
|
|
587
604
|
if token_set and token_set.get("expires_at", 0) > time.time():
|
|
588
605
|
return token_set["access_token"]
|
|
589
|
-
|
|
606
|
+
|
|
590
607
|
# Check for refresh token
|
|
591
608
|
if not state_data_dict or not state_data_dict.get("refresh_token"):
|
|
592
609
|
raise AccessTokenError(
|
|
593
610
|
AccessTokenErrorCode.MISSING_REFRESH_TOKEN,
|
|
594
611
|
"The access token has expired and a refresh token was not provided. The user needs to re-authenticate."
|
|
595
612
|
)
|
|
596
|
-
|
|
613
|
+
|
|
597
614
|
# Get new token with refresh token
|
|
598
615
|
try:
|
|
599
616
|
token_endpoint_response = await self.get_token_by_refresh_token({
|
|
600
617
|
"refresh_token": state_data_dict["refresh_token"]
|
|
601
618
|
})
|
|
602
|
-
|
|
619
|
+
|
|
603
620
|
# Update state data with new token
|
|
604
621
|
existing_state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
605
|
-
updated_state_data = State.update_state_data(
|
|
606
|
-
|
|
622
|
+
updated_state_data = State.update_state_data(
|
|
623
|
+
audience, existing_state_data, token_endpoint_response)
|
|
624
|
+
|
|
607
625
|
# Store updated state
|
|
608
626
|
await self._state_store.set(self._state_identifier, updated_state_data, options=store_options)
|
|
609
|
-
|
|
627
|
+
|
|
610
628
|
return token_endpoint_response["access_token"]
|
|
611
629
|
except Exception as e:
|
|
612
630
|
if isinstance(e, AccessTokenError):
|
|
@@ -623,21 +641,21 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
623
641
|
) -> str:
|
|
624
642
|
"""
|
|
625
643
|
Retrieves an access token for a connection.
|
|
626
|
-
|
|
644
|
+
|
|
627
645
|
This method attempts to obtain an access token for a specified connection.
|
|
628
646
|
It first checks if a refresh token exists in the store.
|
|
629
647
|
If no refresh token is found, it throws an `AccessTokenForConnectionError` indicating
|
|
630
648
|
that the refresh token was not found.
|
|
631
|
-
|
|
649
|
+
|
|
632
650
|
Args:
|
|
633
651
|
options: Options for retrieving an access token for a connection.
|
|
634
652
|
store_options: Optional options used to pass to the Transaction and State Store.
|
|
635
|
-
|
|
653
|
+
|
|
636
654
|
Returns:
|
|
637
655
|
The access token for the connection
|
|
638
|
-
|
|
656
|
+
|
|
639
657
|
Raises:
|
|
640
|
-
AccessTokenForConnectionError: If the access token was not found or
|
|
658
|
+
AccessTokenForConnectionError: If the access token was not found or
|
|
641
659
|
there was an issue requesting the access token.
|
|
642
660
|
"""
|
|
643
661
|
state_data = await self._state_store.get(self._state_identifier, store_options)
|
|
@@ -646,7 +664,7 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
646
664
|
state_data_dict = state_data.dict()
|
|
647
665
|
else:
|
|
648
666
|
state_data_dict = state_data or {}
|
|
649
|
-
|
|
667
|
+
|
|
650
668
|
# Find existing connection token
|
|
651
669
|
connection_token_set = None
|
|
652
670
|
if state_data_dict and len(state_data_dict["connection_token_sets"]) > 0:
|
|
@@ -654,11 +672,11 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
654
672
|
if ts.get("connection") == options["connection"]:
|
|
655
673
|
connection_token_set = ts
|
|
656
674
|
break
|
|
657
|
-
|
|
675
|
+
|
|
658
676
|
# If token is valid, return it
|
|
659
677
|
if connection_token_set and connection_token_set.get("expires_at", 0) > time.time():
|
|
660
678
|
return connection_token_set["access_token"]
|
|
661
|
-
|
|
679
|
+
|
|
662
680
|
# Check for refresh token
|
|
663
681
|
if not state_data_dict or not state_data_dict.get("refresh_token"):
|
|
664
682
|
raise AccessTokenForConnectionError(
|
|
@@ -671,70 +689,72 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
671
689
|
"login_hint": options.get("login_hint"),
|
|
672
690
|
"refresh_token": state_data_dict["refresh_token"]
|
|
673
691
|
})
|
|
674
|
-
|
|
692
|
+
|
|
675
693
|
# Update state data with new token
|
|
676
|
-
updated_state_data = State.update_state_data_for_connection_token_set(
|
|
677
|
-
|
|
694
|
+
updated_state_data = State.update_state_data_for_connection_token_set(
|
|
695
|
+
options, state_data_dict, token_endpoint_response)
|
|
696
|
+
|
|
678
697
|
# Store updated state
|
|
679
698
|
await self._state_store.set(self._state_identifier, updated_state_data, store_options)
|
|
680
|
-
|
|
699
|
+
|
|
681
700
|
return token_endpoint_response["access_token"]
|
|
682
|
-
|
|
683
701
|
|
|
684
702
|
async def logout(
|
|
685
|
-
self,
|
|
703
|
+
self,
|
|
686
704
|
options: Optional[LogoutOptions] = None,
|
|
687
705
|
store_options: Optional[Dict[str, Any]] = None
|
|
688
706
|
) -> str:
|
|
689
707
|
options = options or LogoutOptions()
|
|
690
|
-
|
|
708
|
+
|
|
691
709
|
# Delete the session from the state store
|
|
692
710
|
await self._state_store.delete(self._state_identifier, store_options)
|
|
693
|
-
|
|
711
|
+
|
|
694
712
|
# Use the URL helper to create the logout URL.
|
|
695
|
-
logout_url = URL.create_logout_url(
|
|
696
|
-
|
|
713
|
+
logout_url = URL.create_logout_url(
|
|
714
|
+
self._domain, self._client_id, options.return_to)
|
|
715
|
+
|
|
697
716
|
return logout_url
|
|
698
|
-
|
|
717
|
+
|
|
699
718
|
async def handle_backchannel_logout(
|
|
700
|
-
self,
|
|
701
|
-
logout_token: str,
|
|
719
|
+
self,
|
|
720
|
+
logout_token: str,
|
|
702
721
|
store_options: Optional[Dict[str, Any]] = None
|
|
703
722
|
) -> None:
|
|
704
723
|
"""
|
|
705
724
|
Handles backchannel logout requests.
|
|
706
|
-
|
|
725
|
+
|
|
707
726
|
Args:
|
|
708
727
|
logout_token: The logout token sent by Auth0
|
|
709
728
|
store_options: Options to pass to the state store
|
|
710
729
|
"""
|
|
711
730
|
if not logout_token:
|
|
712
731
|
raise BackchannelLogoutError("Missing logout token")
|
|
713
|
-
|
|
732
|
+
|
|
714
733
|
try:
|
|
715
734
|
# Decode the token without verification
|
|
716
|
-
claims = jwt.decode(logout_token, options={
|
|
717
|
-
|
|
735
|
+
claims = jwt.decode(logout_token, options={
|
|
736
|
+
"verify_signature": False})
|
|
737
|
+
|
|
718
738
|
# Validate the token is a logout token
|
|
719
739
|
events = claims.get("events", {})
|
|
720
740
|
if "http://schemas.openid.net/event/backchannel-logout" not in events:
|
|
721
|
-
raise BackchannelLogoutError(
|
|
722
|
-
|
|
741
|
+
raise BackchannelLogoutError(
|
|
742
|
+
"Invalid logout token: not a backchannel logout event")
|
|
743
|
+
|
|
723
744
|
# Delete sessions associated with this token
|
|
724
745
|
logout_claims = LogoutTokenClaims(
|
|
725
746
|
sub=claims.get("sub"),
|
|
726
747
|
sid=claims.get("sid")
|
|
727
748
|
)
|
|
728
|
-
|
|
749
|
+
|
|
729
750
|
await self._state_store.delete_by_logout_token(logout_claims.dict(), store_options)
|
|
730
|
-
|
|
731
|
-
except (jwt.JoseError, ValidationError) as e:
|
|
732
|
-
raise BackchannelLogoutError(f"Error processing logout token: {str(e)}")
|
|
733
|
-
|
|
734
751
|
|
|
752
|
+
except (jwt.JoseError, ValidationError) as e:
|
|
753
|
+
raise BackchannelLogoutError(
|
|
754
|
+
f"Error processing logout token: {str(e)}")
|
|
735
755
|
|
|
736
756
|
# Authlib Helpers
|
|
737
|
-
|
|
757
|
+
|
|
738
758
|
async def _build_link_user_url(
|
|
739
759
|
self,
|
|
740
760
|
connection: str,
|
|
@@ -747,15 +767,15 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
747
767
|
"""Build a URL for linking user accounts"""
|
|
748
768
|
# Generate code challenge from verifier
|
|
749
769
|
code_challenge = PKCE.generate_code_challenge(code_verifier)
|
|
750
|
-
|
|
770
|
+
|
|
751
771
|
# Get metadata if not already fetched
|
|
752
772
|
if not hasattr(self, '_oauth_metadata'):
|
|
753
773
|
self._oauth_metadata = await self._fetch_oidc_metadata(self._domain)
|
|
754
|
-
|
|
774
|
+
|
|
755
775
|
# Get authorization endpoint
|
|
756
|
-
auth_endpoint = self._oauth_metadata.get("authorization_endpoint",
|
|
757
|
-
|
|
758
|
-
|
|
776
|
+
auth_endpoint = self._oauth_metadata.get("authorization_endpoint",
|
|
777
|
+
f"https://{self._domain}/authorize")
|
|
778
|
+
|
|
759
779
|
# Build params
|
|
760
780
|
params = {
|
|
761
781
|
"client_id": self._client_id,
|
|
@@ -769,16 +789,16 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
769
789
|
"scope": "openid link_account",
|
|
770
790
|
"audience": "my-account"
|
|
771
791
|
}
|
|
772
|
-
|
|
792
|
+
|
|
773
793
|
# Add connection scope if provided
|
|
774
794
|
if connection_scope:
|
|
775
795
|
params["requested_connection_scope"] = connection_scope
|
|
776
|
-
|
|
796
|
+
|
|
777
797
|
# Add any additional parameters
|
|
778
798
|
if authorization_params:
|
|
779
799
|
params.update(authorization_params)
|
|
780
800
|
return URL.build_url(auth_endpoint, params)
|
|
781
|
-
|
|
801
|
+
|
|
782
802
|
async def _build_unlink_user_url(
|
|
783
803
|
self,
|
|
784
804
|
connection: str,
|
|
@@ -790,15 +810,15 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
790
810
|
"""Build a URL for unlinking user accounts"""
|
|
791
811
|
# Generate code challenge from verifier
|
|
792
812
|
code_challenge = PKCE.generate_code_challenge(code_verifier)
|
|
793
|
-
|
|
813
|
+
|
|
794
814
|
# Get metadata if not already fetched
|
|
795
815
|
if not hasattr(self, '_oauth_metadata'):
|
|
796
816
|
self._oauth_metadata = await self._fetch_oidc_metadata(self._domain)
|
|
797
|
-
|
|
817
|
+
|
|
798
818
|
# Get authorization endpoint
|
|
799
|
-
auth_endpoint = self._oauth_metadata.get("authorization_endpoint",
|
|
800
|
-
|
|
801
|
-
|
|
819
|
+
auth_endpoint = self._oauth_metadata.get("authorization_endpoint",
|
|
820
|
+
f"https://{self._domain}/authorize")
|
|
821
|
+
|
|
802
822
|
# Build params
|
|
803
823
|
params = {
|
|
804
824
|
"client_id": self._client_id,
|
|
@@ -814,26 +834,26 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
814
834
|
# Add any additional parameters
|
|
815
835
|
if authorization_params:
|
|
816
836
|
params.update(authorization_params)
|
|
817
|
-
|
|
837
|
+
|
|
818
838
|
return URL.build_url(auth_endpoint, params)
|
|
819
|
-
|
|
839
|
+
|
|
820
840
|
async def backchannel_authentication(
|
|
821
841
|
self,
|
|
822
842
|
options: Dict[str, Any]
|
|
823
843
|
) -> Dict[str, Any]:
|
|
824
844
|
"""
|
|
825
845
|
Initiates backchannel authentication with Auth0.
|
|
826
|
-
|
|
846
|
+
|
|
827
847
|
This method starts a Client-Initiated Backchannel Authentication (CIBA) flow,
|
|
828
848
|
which allows an application to request authentication from a user via a separate
|
|
829
849
|
device or channel.
|
|
830
|
-
|
|
850
|
+
|
|
831
851
|
Args:
|
|
832
852
|
options: Configuration options for backchannel authentication
|
|
833
|
-
|
|
853
|
+
|
|
834
854
|
Returns:
|
|
835
855
|
Token response data from the backchannel authentication
|
|
836
|
-
|
|
856
|
+
|
|
837
857
|
Raises:
|
|
838
858
|
ApiError: If the backchannel authentication fails
|
|
839
859
|
"""
|
|
@@ -841,56 +861,57 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
841
861
|
# Fetch OpenID Connect metadata if not already fetched
|
|
842
862
|
if not hasattr(self, '_oauth_metadata'):
|
|
843
863
|
self._oauth_metadata = await self._fetch_oidc_metadata(self._domain)
|
|
844
|
-
|
|
864
|
+
|
|
845
865
|
# Get the issuer from metadata
|
|
846
|
-
issuer = self._oauth_metadata.get(
|
|
847
|
-
|
|
866
|
+
issuer = self._oauth_metadata.get(
|
|
867
|
+
"issuer") or f"https://{self._domain}/"
|
|
868
|
+
|
|
848
869
|
# Get backchannel authentication endpoint
|
|
849
|
-
backchannel_endpoint = self._oauth_metadata.get(
|
|
870
|
+
backchannel_endpoint = self._oauth_metadata.get(
|
|
871
|
+
"backchannel_authentication_endpoint")
|
|
850
872
|
if not backchannel_endpoint:
|
|
851
873
|
raise ApiError(
|
|
852
|
-
"configuration_error",
|
|
874
|
+
"configuration_error",
|
|
853
875
|
"Backchannel authentication is not supported by the authorization server"
|
|
854
876
|
)
|
|
855
|
-
|
|
877
|
+
|
|
856
878
|
# Get token endpoint
|
|
857
879
|
token_endpoint = self._oauth_metadata.get("token_endpoint")
|
|
858
880
|
if not token_endpoint:
|
|
859
881
|
raise ApiError(
|
|
860
|
-
"configuration_error",
|
|
882
|
+
"configuration_error",
|
|
861
883
|
"Token endpoint is missing in OIDC metadata"
|
|
862
884
|
)
|
|
863
|
-
|
|
864
|
-
sub = sub = options.get('login_hint', {}).get("sub")
|
|
885
|
+
|
|
886
|
+
sub = sub = options.get('login_hint', {}).get("sub")
|
|
865
887
|
if not sub:
|
|
866
888
|
raise ApiError(
|
|
867
889
|
"invalid_parameter",
|
|
868
890
|
"login_hint must contain a 'sub' field"
|
|
869
891
|
)
|
|
870
|
-
|
|
892
|
+
|
|
871
893
|
# Prepare login hint in the required format
|
|
872
894
|
login_hint = json.dumps({
|
|
873
895
|
"format": "iss_sub",
|
|
874
896
|
"iss": issuer,
|
|
875
|
-
"sub": sub
|
|
897
|
+
"sub": sub
|
|
876
898
|
})
|
|
877
|
-
|
|
899
|
+
|
|
878
900
|
# The Request Parameters
|
|
879
901
|
params = {
|
|
880
902
|
"client_id": self._client_id,
|
|
881
|
-
"scope": "openid profile email", # DEFAULT_SCOPES
|
|
903
|
+
"scope": "openid profile email", # DEFAULT_SCOPES
|
|
882
904
|
"login_hint": login_hint,
|
|
883
905
|
}
|
|
884
|
-
|
|
885
|
-
|
|
906
|
+
|
|
886
907
|
# Add binding message if provided
|
|
887
908
|
if options.get('binding_message'):
|
|
888
909
|
params["binding_message"] = options.get('binding_message')
|
|
889
|
-
|
|
910
|
+
|
|
890
911
|
# Add any additional authorization parameters
|
|
891
912
|
if self._default_authorization_params:
|
|
892
913
|
params.update(self._default_authorization_params)
|
|
893
|
-
|
|
914
|
+
|
|
894
915
|
if options.get('authorization_params'):
|
|
895
916
|
params.update(options.get('authorization_params'))
|
|
896
917
|
|
|
@@ -906,20 +927,23 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
906
927
|
error_data = backchannel_response.json()
|
|
907
928
|
raise ApiError(
|
|
908
929
|
error_data.get("error", "backchannel_error"),
|
|
909
|
-
error_data.get(
|
|
930
|
+
error_data.get(
|
|
931
|
+
"error_description", "Backchannel authentication request failed")
|
|
910
932
|
)
|
|
911
|
-
|
|
933
|
+
|
|
912
934
|
backchannel_data = backchannel_response.json()
|
|
913
935
|
auth_req_id = backchannel_data.get("auth_req_id")
|
|
914
|
-
expires_in = backchannel_data.get(
|
|
915
|
-
|
|
916
|
-
|
|
936
|
+
expires_in = backchannel_data.get(
|
|
937
|
+
"expires_in", 120) # Default to 2 minutes
|
|
938
|
+
interval = backchannel_data.get(
|
|
939
|
+
"interval", 5) # Default to 5 seconds
|
|
940
|
+
|
|
917
941
|
if not auth_req_id:
|
|
918
942
|
raise ApiError(
|
|
919
|
-
"invalid_response",
|
|
943
|
+
"invalid_response",
|
|
920
944
|
"Missing auth_req_id in backchannel authentication response"
|
|
921
945
|
)
|
|
922
|
-
|
|
946
|
+
|
|
923
947
|
# Poll for token using the auth_req_id
|
|
924
948
|
token_params = {
|
|
925
949
|
"grant_type": "urn:openid:params:grant-type:ciba",
|
|
@@ -927,46 +951,48 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
927
951
|
"client_id": self._client_id,
|
|
928
952
|
"client_secret": self._client_secret
|
|
929
953
|
}
|
|
930
|
-
|
|
954
|
+
|
|
931
955
|
# Calculate when to stop polling
|
|
932
956
|
end_time = time.time() + expires_in
|
|
933
|
-
|
|
957
|
+
|
|
934
958
|
# Poll until we get a response or timeout
|
|
935
959
|
while time.time() < end_time:
|
|
936
960
|
# Make token request
|
|
937
961
|
token_response = await client.post(token_endpoint, data=token_params)
|
|
938
|
-
|
|
962
|
+
|
|
939
963
|
# Check for success (200 OK)
|
|
940
964
|
if token_response.status_code == 200:
|
|
941
965
|
# Success! Parse and return the token response
|
|
942
966
|
return token_response.json()
|
|
943
|
-
|
|
967
|
+
|
|
944
968
|
# Check for specific error that indicates we should continue polling
|
|
945
969
|
if token_response.status_code == 400:
|
|
946
970
|
error_data = token_response.json()
|
|
947
971
|
error = error_data.get("error")
|
|
948
|
-
|
|
972
|
+
|
|
949
973
|
# authorization_pending means we should keep polling
|
|
950
974
|
if error == "authorization_pending":
|
|
951
975
|
# Wait for the specified interval before polling again
|
|
952
976
|
await asyncio.sleep(interval)
|
|
953
977
|
continue
|
|
954
|
-
|
|
978
|
+
|
|
955
979
|
# Other errors should be raised
|
|
956
980
|
raise ApiError(
|
|
957
|
-
error,
|
|
958
|
-
error_data.get("error_description",
|
|
981
|
+
error,
|
|
982
|
+
error_data.get("error_description",
|
|
983
|
+
"Token request failed")
|
|
959
984
|
)
|
|
960
|
-
|
|
985
|
+
|
|
961
986
|
# Any other status code is an error
|
|
962
987
|
raise ApiError(
|
|
963
988
|
"token_error",
|
|
964
989
|
f"Unexpected status code: {token_response.status_code}"
|
|
965
990
|
)
|
|
966
|
-
|
|
991
|
+
|
|
967
992
|
# If we get here, we've timed out
|
|
968
|
-
raise ApiError(
|
|
969
|
-
|
|
993
|
+
raise ApiError(
|
|
994
|
+
"timeout", "Backchannel authentication timed out")
|
|
995
|
+
|
|
970
996
|
except Exception as e:
|
|
971
997
|
print("Caught exception:", type(e), e.args, repr(e))
|
|
972
998
|
raise ApiError(
|
|
@@ -978,41 +1004,42 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
978
1004
|
async def get_token_by_refresh_token(self, options: Dict[str, Any]) -> Dict[str, Any]:
|
|
979
1005
|
"""
|
|
980
1006
|
Retrieves a token by exchanging a refresh token.
|
|
981
|
-
|
|
1007
|
+
|
|
982
1008
|
Args:
|
|
983
1009
|
options: Dictionary containing the refresh token and any additional options.
|
|
984
1010
|
Must include a 'refresh_token' key.
|
|
985
|
-
|
|
1011
|
+
|
|
986
1012
|
Raises:
|
|
987
1013
|
AccessTokenError: If there was an issue requesting the access token.
|
|
988
|
-
|
|
1014
|
+
|
|
989
1015
|
Returns:
|
|
990
1016
|
A dictionary containing the token response from Auth0.
|
|
991
1017
|
"""
|
|
992
1018
|
refresh_token = options.get("refresh_token")
|
|
993
1019
|
if not refresh_token:
|
|
994
1020
|
raise MissingRequiredArgumentError("refresh_token")
|
|
995
|
-
|
|
1021
|
+
|
|
996
1022
|
try:
|
|
997
1023
|
# Ensure we have the OIDC metadata
|
|
998
1024
|
if not hasattr(self._oauth, "metadata") or not self._oauth.metadata:
|
|
999
1025
|
self._oauth.metadata = await self._fetch_oidc_metadata(self._domain)
|
|
1000
|
-
|
|
1026
|
+
|
|
1001
1027
|
token_endpoint = self._oauth.metadata.get("token_endpoint")
|
|
1002
1028
|
if not token_endpoint:
|
|
1003
|
-
raise ApiError("configuration_error",
|
|
1004
|
-
|
|
1029
|
+
raise ApiError("configuration_error",
|
|
1030
|
+
"Token endpoint missing in OIDC metadata")
|
|
1031
|
+
|
|
1005
1032
|
# Prepare the token request parameters
|
|
1006
1033
|
token_params = {
|
|
1007
1034
|
"grant_type": "refresh_token",
|
|
1008
1035
|
"refresh_token": refresh_token,
|
|
1009
1036
|
"client_id": self._client_id,
|
|
1010
1037
|
}
|
|
1011
|
-
|
|
1038
|
+
|
|
1012
1039
|
# Add scope if present in the original authorization params
|
|
1013
1040
|
if "scope" in self._default_authorization_params:
|
|
1014
1041
|
token_params["scope"] = self._default_authorization_params["scope"]
|
|
1015
|
-
|
|
1042
|
+
|
|
1016
1043
|
# Exchange the refresh token for an access token
|
|
1017
1044
|
async with httpx.AsyncClient() as client:
|
|
1018
1045
|
response = await client.post(
|
|
@@ -1020,22 +1047,24 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
1020
1047
|
data=token_params,
|
|
1021
1048
|
auth=(self._client_id, self._client_secret)
|
|
1022
1049
|
)
|
|
1023
|
-
|
|
1050
|
+
|
|
1024
1051
|
if response.status_code != 200:
|
|
1025
1052
|
error_data = response.json()
|
|
1026
1053
|
raise ApiError(
|
|
1027
1054
|
error_data.get("error", "refresh_token_error"),
|
|
1028
|
-
error_data.get("error_description",
|
|
1055
|
+
error_data.get("error_description",
|
|
1056
|
+
"Failed to exchange refresh token")
|
|
1029
1057
|
)
|
|
1030
|
-
|
|
1058
|
+
|
|
1031
1059
|
token_response = response.json()
|
|
1032
|
-
|
|
1060
|
+
|
|
1033
1061
|
# Add required fields if they are missing
|
|
1034
1062
|
if "expires_in" in token_response and "expires_at" not in token_response:
|
|
1035
|
-
token_response["expires_at"] = int(
|
|
1036
|
-
|
|
1063
|
+
token_response["expires_at"] = int(
|
|
1064
|
+
time.time()) + token_response["expires_in"]
|
|
1065
|
+
|
|
1037
1066
|
return token_response
|
|
1038
|
-
|
|
1067
|
+
|
|
1039
1068
|
except Exception as e:
|
|
1040
1069
|
if isinstance(e, ApiError):
|
|
1041
1070
|
raise
|
|
@@ -1044,19 +1073,19 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
1044
1073
|
"The access token has expired and there was an error while trying to refresh it.",
|
|
1045
1074
|
e
|
|
1046
1075
|
)
|
|
1047
|
-
|
|
1076
|
+
|
|
1048
1077
|
async def get_token_for_connection(self, options: Dict[str, Any]) -> Dict[str, Any]:
|
|
1049
1078
|
"""
|
|
1050
1079
|
Retrieves a token for a connection.
|
|
1051
|
-
|
|
1080
|
+
|
|
1052
1081
|
Args:
|
|
1053
1082
|
options: Options for retrieving an access token for a connection.
|
|
1054
1083
|
Must include 'connection' and 'refresh_token' keys.
|
|
1055
1084
|
May optionally include 'login_hint'.
|
|
1056
|
-
|
|
1085
|
+
|
|
1057
1086
|
Raises:
|
|
1058
1087
|
AccessTokenForConnectionError: If there was an issue requesting the access token.
|
|
1059
|
-
|
|
1088
|
+
|
|
1060
1089
|
Returns:
|
|
1061
1090
|
Dictionary containing the token response with accessToken, expiresAt, and scope.
|
|
1062
1091
|
"""
|
|
@@ -1068,11 +1097,12 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
1068
1097
|
# Ensure we have OIDC metadata
|
|
1069
1098
|
if not hasattr(self._oauth, "metadata") or not self._oauth.metadata:
|
|
1070
1099
|
self._oauth.metadata = await self._fetch_oidc_metadata(self._domain)
|
|
1071
|
-
|
|
1100
|
+
|
|
1072
1101
|
token_endpoint = self._oauth.metadata.get("token_endpoint")
|
|
1073
1102
|
if not token_endpoint:
|
|
1074
|
-
raise ApiError("configuration_error",
|
|
1075
|
-
|
|
1103
|
+
raise ApiError("configuration_error",
|
|
1104
|
+
"Token endpoint missing in OIDC metadata")
|
|
1105
|
+
|
|
1076
1106
|
# Prepare parameters
|
|
1077
1107
|
params = {
|
|
1078
1108
|
"connection": options["connection"],
|
|
@@ -1082,7 +1112,7 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
1082
1112
|
"grant_type": GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN,
|
|
1083
1113
|
"client_id": self._client_id
|
|
1084
1114
|
}
|
|
1085
|
-
|
|
1115
|
+
|
|
1086
1116
|
# Add login_hint if provided
|
|
1087
1117
|
if "login_hint" in options and options["login_hint"]:
|
|
1088
1118
|
params["login_hint"] = options["login_hint"]
|
|
@@ -1090,26 +1120,28 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
1090
1120
|
# Make the request
|
|
1091
1121
|
async with httpx.AsyncClient() as client:
|
|
1092
1122
|
response = await client.post(
|
|
1093
|
-
token_endpoint,
|
|
1123
|
+
token_endpoint,
|
|
1094
1124
|
data=params,
|
|
1095
1125
|
auth=(self._client_id, self._client_secret)
|
|
1096
1126
|
)
|
|
1097
1127
|
|
|
1098
1128
|
if response.status_code != 200:
|
|
1099
|
-
error_data = response.json() if response.headers.get(
|
|
1129
|
+
error_data = response.json() if response.headers.get(
|
|
1130
|
+
"content-type") == "application/json" else {}
|
|
1100
1131
|
raise ApiError(
|
|
1101
1132
|
error_data.get("error", "connection_token_error"),
|
|
1102
|
-
error_data.get(
|
|
1133
|
+
error_data.get(
|
|
1134
|
+
"error_description", f"Failed to get token for connection: {response.status_code}")
|
|
1103
1135
|
)
|
|
1104
|
-
|
|
1136
|
+
|
|
1105
1137
|
token_endpoint_response = response.json()
|
|
1106
|
-
|
|
1138
|
+
|
|
1107
1139
|
return {
|
|
1108
1140
|
"access_token": token_endpoint_response.get("access_token"),
|
|
1109
1141
|
"expires_at": int(time.time()) + int(token_endpoint_response.get("expires_in", 3600)),
|
|
1110
1142
|
"scope": token_endpoint_response.get("scope", "")
|
|
1111
1143
|
}
|
|
1112
|
-
|
|
1144
|
+
|
|
1113
1145
|
except Exception as e:
|
|
1114
1146
|
if isinstance(e, ApiError):
|
|
1115
1147
|
raise AccessTokenForConnectionError(
|
|
@@ -1120,4 +1152,4 @@ class ServerClient(Generic[TStoreOptions]):
|
|
|
1120
1152
|
AccessTokenForConnectionErrorCode.FETCH_ERROR,
|
|
1121
1153
|
"There was an error while trying to retrieve an access token for a connection.",
|
|
1122
1154
|
e
|
|
1123
|
-
)
|
|
1155
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{auth0_server_python-1.0.0b3 → auth0_server_python-1.0.0b4}/src/auth0_server_python/utils/helpers.py
RENAMED
|
File without changes
|