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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auth0-server-python
3
- Version: 1.0.0b3
3
+ Version: 1.0.0b4
4
4
  Summary: Auth0 server-side Python SDK
5
5
  License: MIT
6
6
  Author: Auth0
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0.b3"
3
+ version = "1.0.0.b4"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -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 = None,
60
- state_store = None,
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(options.authorization_params)
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", "Failed to fetch OIDC metadata", e)
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("pushed_authorization_request_endpoint")
177
+ par_endpoint = self._oauth.metadata.get(
178
+ "pushed_authorization_request_endpoint")
172
179
  if not par_endpoint:
173
- raise ApiError("configuration_error", "PAR is enabled but pushed_authorization_request_endpoint is missing in metadata")
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("error_description", "Failed to obtain request_uri from PAR endpoint")
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("par_error", "No request_uri returned from PAR endpoint")
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", "Authorization endpoint missing in OIDC metadata")
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(authorization_endpoint, **auth_params)
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", "Failed to create authorization URL", e)
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("token_error", f"Token exchange failed: {str(e)}", e)
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={"verify_signature": False})
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()) + token_response.get("expires_in", 3600)
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("sid") if user_info and "sid" in user_info else PKCE.generate_random_string(32)
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
- refresh_token=token_response.get("refresh_token"), # might be None if not provided
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("audience", "default")
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() if k != "internal"}
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(audience, existing_state_data, token_endpoint_response)
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(options, state_data_dict, token_endpoint_response)
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(self._domain, self._client_id, options.return_to)
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={"verify_signature": False})
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("Invalid logout token: not a backchannel logout event")
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
- f"https://{self._domain}/authorize")
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
- f"https://{self._domain}/authorize")
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("issuer") or f"https://{self._domain}/"
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("backchannel_authentication_endpoint")
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("error_description", "Backchannel authentication request failed")
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("expires_in", 120) # Default to 2 minutes
915
- interval = backchannel_data.get("interval", 5) # Default to 5 seconds
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", "Token request failed")
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("timeout", "Backchannel authentication timed out")
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", "Token endpoint missing in OIDC metadata")
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", "Failed to exchange refresh token")
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(time.time()) + token_response["expires_in"]
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", "Token endpoint missing in OIDC metadata")
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("content-type") == "application/json" else {}
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("error_description", f"Failed to get token for connection: {response.status_code}")
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
+ )