auth0-server-python 1.0.0b10__tar.gz → 1.0.0b12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/PKG-INFO +11 -1
  2. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/README.md +10 -0
  3. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/pyproject.toml +1 -1
  4. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_server/server_client.py +150 -6
  5. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_types/__init__.py +30 -41
  6. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/error/__init__.py +27 -0
  7. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/tests/test_server_client.py +2023 -35
  8. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/utils/helpers.py +82 -2
  9. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/LICENSE +0 -0
  10. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
  11. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
  12. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_server/__init__.py +0 -0
  13. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_server/mfa_client.py +0 -0
  14. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/auth_server/my_account_client.py +0 -0
  15. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/encryption/__init__.py +0 -0
  16. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/encryption/encrypt.py +0 -0
  17. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/store/__init__.py +0 -0
  18. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/store/abstract.py +0 -0
  19. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/telemetry.py +0 -0
  20. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/tests/test_mfa_client.py +0 -0
  21. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/tests/test_my_account_client.py +0 -0
  22. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/tests/test_telemetry.py +0 -0
  23. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b12}/src/auth0_server_python/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth0-server-python
3
- Version: 1.0.0b10
3
+ Version: 1.0.0b12
4
4
  Summary: Auth0 server-side Python SDK
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -129,6 +129,10 @@ async def callback(request: Request):
129
129
  return RedirectResponse(url="/")
130
130
  ```
131
131
 
132
+ #### Organizations
133
+
134
+ The SDK supports [Auth0 Organizations](https://auth0.com/docs/organizations) with first-class `organization` and `invitation` parameters on `ServerClient` and `StartInteractiveLoginOptions`. Token claim validation is enforced automatically at callback. For setup, invitation flows, error handling, and reading org data from the session, see [examples/InteractiveLogin.md](examples/InteractiveLogin.md#8-organizations).
135
+
132
136
  ### 4. Login with Custom Token Exchange
133
137
 
134
138
  If you're migrating from a legacy authentication system or integrating with a custom identity provider, you can exchange external tokens for Auth0 tokens using the OAuth 2.0 Token Exchange specification (RFC 8693):
@@ -198,6 +202,12 @@ The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and
198
202
 
199
203
  For more details and examples, see [examples/MultipleCustomDomains.md](examples/MultipleCustomDomains.md).
200
204
 
205
+ ### 6. Session Expiry from the Upstream IdP
206
+
207
+ For enterprise connections, the upstream identity provider can cap how long a user's session lives. When the connection is configured to honor it, Auth0 includes a `session_expiry` claim in the ID token, and the SDK enforces this ceiling on every session read. Once it is reached, `get_user()` and `get_session()` return `None`, and `get_access_token()` raises an `AccessTokenError` with code `session_expired`. If the asserted ceiling is already in the past at login, `complete_interactive_login()` raises a `SessionExpiredError` instead of persisting an already-expired session.
208
+
209
+ For more details and examples, see [examples/RetrievingData.md](examples/RetrievingData.md#session-expiry-from-the-upstream-idp).
210
+
201
211
  ## Feedback
202
212
 
203
213
  ### Contributing
@@ -104,6 +104,10 @@ async def callback(request: Request):
104
104
  return RedirectResponse(url="/")
105
105
  ```
106
106
 
107
+ #### Organizations
108
+
109
+ The SDK supports [Auth0 Organizations](https://auth0.com/docs/organizations) with first-class `organization` and `invitation` parameters on `ServerClient` and `StartInteractiveLoginOptions`. Token claim validation is enforced automatically at callback. For setup, invitation flows, error handling, and reading org data from the session, see [examples/InteractiveLogin.md](examples/InteractiveLogin.md#8-organizations).
110
+
107
111
  ### 4. Login with Custom Token Exchange
108
112
 
109
113
  If you're migrating from a legacy authentication system or integrating with a custom identity provider, you can exchange external tokens for Auth0 tokens using the OAuth 2.0 Token Exchange specification (RFC 8693):
@@ -173,6 +177,12 @@ The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and
173
177
 
174
178
  For more details and examples, see [examples/MultipleCustomDomains.md](examples/MultipleCustomDomains.md).
175
179
 
180
+ ### 6. Session Expiry from the Upstream IdP
181
+
182
+ For enterprise connections, the upstream identity provider can cap how long a user's session lives. When the connection is configured to honor it, Auth0 includes a `session_expiry` claim in the ID token, and the SDK enforces this ceiling on every session read. Once it is reached, `get_user()` and `get_session()` return `None`, and `get_access_token()` raises an `AccessTokenError` with code `session_expired`. If the asserted ceiling is already in the past at login, `complete_interactive_login()` raises a `SessionExpiredError` instead of persisting an already-expired session.
183
+
184
+ For more details and examples, see [examples/RetrievingData.md](examples/RetrievingData.md#session-expiry-from-the-upstream-idp).
185
+
176
186
  ## Feedback
177
187
 
178
188
  ### Contributing
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0b10"
3
+ version = "1.0.0b12"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -54,13 +54,16 @@ from auth0_server_python.error import (
54
54
  MfaRequiredError,
55
55
  MissingRequiredArgumentError,
56
56
  MissingTransactionError,
57
+ OrganizationTokenValidationError,
57
58
  PollingApiError,
59
+ SessionExpiredError,
58
60
  StartLinkUserError,
59
61
  )
60
62
  from auth0_server_python.telemetry import Telemetry
61
63
  from auth0_server_python.utils import PKCE, URL, State
62
64
  from auth0_server_python.utils.helpers import (
63
65
  build_domain_resolver_context,
66
+ validate_org_claims,
64
67
  validate_resolved_domain_value,
65
68
  )
66
69
 
@@ -96,6 +99,7 @@ class ServerClient(Generic[TStoreOptions]):
96
99
  state_identifier: str = "_a0_session",
97
100
  authorization_params: Optional[dict[str, Any]] = None,
98
101
  pushed_authorization_requests: bool = False,
102
+ organization: Optional[str] = None,
99
103
  ):
100
104
  """
101
105
  Initialize the Auth0 server client.
@@ -112,6 +116,9 @@ class ServerClient(Generic[TStoreOptions]):
112
116
  state_identifier: Identifier for state data
113
117
  authorization_params: Default parameters for authorization requests
114
118
  pushed_authorization_requests: Whether to use Pushed Authorization Requests
119
+ organization: Default organization for all login flows from this client.
120
+ Can be an org ID (e.g. 'org_abc123') or an org name (e.g. 'acme-corp').
121
+ Per-login values passed in StartInteractiveLoginOptions always override this.
115
122
  """
116
123
  if not secret:
117
124
  raise MissingRequiredArgumentError("secret")
@@ -146,6 +153,7 @@ class ServerClient(Generic[TStoreOptions]):
146
153
  self._secret = secret
147
154
  self._default_authorization_params = authorization_params or {}
148
155
  self._pushed_authorization_requests = pushed_authorization_requests # store the flag
156
+ self._organization = organization
149
157
 
150
158
  # Initialize stores
151
159
  self._transaction_store = transaction_store
@@ -207,6 +215,7 @@ class ServerClient(Generic[TStoreOptions]):
207
215
 
208
216
  return value.rstrip('/')
209
217
 
218
+
210
219
  async def _resolve_current_domain(self, store_options=None) -> str:
211
220
  """Resolve domain from resolver function or return static domain."""
212
221
  if self._domain_resolver:
@@ -502,6 +511,16 @@ class ServerClient(Generic[TStoreOptions]):
502
511
  merged_scope = self._merge_scope_with_defaults(requested_scope, audience)
503
512
  auth_params["scope"] = merged_scope
504
513
 
514
+ # Typed org/invitation fields win over anything already in auth_params from authorization_params.
515
+ resolved_org = options.organization or self._organization
516
+ if resolved_org and not resolved_org.strip():
517
+ raise InvalidArgumentError("organization", "organization must not be blank")
518
+ if resolved_org:
519
+ auth_params["organization"] = resolved_org
520
+
521
+ if options.invitation:
522
+ auth_params["invitation"] = options.invitation
523
+
505
524
  # Build the transaction data to store with domain
506
525
  transaction_data = TransactionData(
507
526
  code_verifier=code_verifier,
@@ -509,6 +528,7 @@ class ServerClient(Generic[TStoreOptions]):
509
528
  audience=audience,
510
529
  domain=origin_domain,
511
530
  redirect_uri=auth_params.get("redirect_uri"),
531
+ organization=resolved_org,
512
532
  )
513
533
 
514
534
  # Store the transaction data
@@ -637,9 +657,32 @@ class ServerClient(Generic[TStoreOptions]):
637
657
  # Use the userinfo field from the token_response for user claims
638
658
  user_info = token_response.get("userinfo")
639
659
  user_claims = None
660
+ # IPSIE session_expiry ceiling, read from the verified ID token claims.
661
+ session_expires_at = None
662
+ # ID token `iat`, used to detect a ceiling that is already past at login.
663
+ issued_at = None
640
664
  id_token = token_response.get("id_token")
641
665
 
666
+ expected_org = transaction_data.organization
667
+
668
+ if not user_info and not id_token and expected_org:
669
+ raise OrganizationTokenValidationError(
670
+ "Organization was requested but the token response included neither an ID token nor userinfo; "
671
+ "cannot verify organization membership"
672
+ )
673
+
642
674
  if user_info:
675
+ if not isinstance(user_info, dict):
676
+ if expected_org:
677
+ raise OrganizationTokenValidationError(
678
+ "Userinfo response is not a valid claims dictionary; cannot verify organization membership"
679
+ )
680
+ raise ApiError(
681
+ "invalid_response",
682
+ "Userinfo response is not a valid claims dictionary"
683
+ )
684
+ if expected_org:
685
+ validate_org_claims(user_info, expected_org)
643
686
  user_claims = UserClaims.parse_obj(user_info)
644
687
  elif id_token:
645
688
  # Fetch JWKS for signature verification
@@ -656,7 +699,13 @@ class ServerClient(Generic[TStoreOptions]):
656
699
  if self._normalize_url(token_issuer) != self._normalize_url(origin_issuer):
657
700
  raise IssuerValidationError("ID token issuer mismatch. Ensure your Auth0 domain is configured correctly.")
658
701
 
702
+ # Organization claim validation — mandatory when org was requested.
703
+ if expected_org:
704
+ validate_org_claims(claims, expected_org)
705
+
659
706
  user_claims = UserClaims.parse_obj(claims)
707
+ session_expires_at = user_claims.session_expiry
708
+ issued_at = claims.get("iat")
660
709
  except ValueError as e:
661
710
  raise ApiError("jwks_key_not_found", str(e))
662
711
  except jwt.InvalidSignatureError as e:
@@ -685,6 +734,11 @@ class ServerClient(Generic[TStoreOptions]):
685
734
  )
686
735
 
687
736
 
737
+ # Refuse to persist a session whose ceiling is already in the past.
738
+ if State.is_session_ceiling_in_past(session_expires_at, issued_at):
739
+ await self._transaction_store.delete(transaction_identifier, options=store_options)
740
+ raise SessionExpiredError()
741
+
688
742
  # Build a token set using the token response data
689
743
  token_set = TokenSet(
690
744
  audience=transaction_data.audience or self.DEFAULT_AUDIENCE_STATE_KEY,
@@ -708,7 +762,8 @@ class ServerClient(Generic[TStoreOptions]):
708
762
  domain=origin_domain,
709
763
  internal={
710
764
  "sid": sid,
711
- "created_at": int(time.time())
765
+ "created_at": int(time.time()),
766
+ "session_expires_at": session_expires_at
712
767
  }
713
768
  )
714
769
 
@@ -734,6 +789,23 @@ class ServerClient(Generic[TStoreOptions]):
734
789
  # Methods for retrieving user information, session data, and logout operations.
735
790
  # ============================================================================
736
791
 
792
+ async def _is_session_expired_by_ceiling(
793
+ self, state_data_dict: dict, store_options: Optional[dict[str, Any]] = None
794
+ ) -> bool:
795
+ """
796
+ Enforce the IPSIE session_expiry ceiling on a session read.
797
+
798
+ Returns True (and deletes the stored session) when the upstream
799
+ IdP-asserted ceiling has been reached. Sessions without a
800
+ session_expires_at value are never expired on this basis.
801
+ """
802
+ internal = state_data_dict.get("internal") or {}
803
+ session_expires_at = internal.get("session_expires_at")
804
+ if State.is_session_ceiling_reached(session_expires_at):
805
+ await self._state_store.delete(self._state_identifier, options=store_options)
806
+ return True
807
+ return False
808
+
737
809
  async def get_user(self, store_options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
738
810
  """
739
811
  Retrieves the user from the store, or None if no user found.
@@ -760,6 +832,10 @@ class ServerClient(Generic[TStoreOptions]):
760
832
  if self._normalize_url(session_domain) != self._normalize_url(current_domain):
761
833
  return None
762
834
 
835
+ # IPSIE: force re-auth once the upstream IdP session ceiling passes.
836
+ if await self._is_session_expired_by_ceiling(state_data, store_options):
837
+ return None
838
+
763
839
  return state_data.get("user")
764
840
  return None
765
841
 
@@ -789,6 +865,10 @@ class ServerClient(Generic[TStoreOptions]):
789
865
  if self._normalize_url(session_domain) != self._normalize_url(current_domain):
790
866
  return None
791
867
 
868
+ # IPSIE: force re-auth once the upstream IdP session ceiling passes.
869
+ if await self._is_session_expired_by_ceiling(state_data, store_options):
870
+ return None
871
+
792
872
  session_data = {k: v for k, v in state_data.items()
793
873
  if k != "internal"}
794
874
  return session_data
@@ -972,6 +1052,12 @@ class ServerClient(Generic[TStoreOptions]):
972
1052
 
973
1053
  merged_scope = self._merge_scope_with_defaults(scope, audience)
974
1054
 
1055
+ # Once the session ceiling has passed, fail instead of serving or refreshing a token.
1056
+ internal = (state_data_dict or {}).get("internal") or {}
1057
+ if State.is_session_ceiling_reached(internal.get("session_expires_at")):
1058
+ await self._state_store.delete(self._state_identifier, options=store_options)
1059
+ raise SessionExpiredError()
1060
+
975
1061
  # Find matching token set
976
1062
  token_set = None
977
1063
  if state_data_dict and "token_sets" in state_data_dict:
@@ -1283,7 +1369,10 @@ class ServerClient(Generic[TStoreOptions]):
1283
1369
  while time.time() < end_time:
1284
1370
  # Make token request
1285
1371
  try:
1286
- token_response = await self.backchannel_authentication_grant(auth_req_id, store_options=store_options)
1372
+ token_response = await self.backchannel_authentication_grant(
1373
+ auth_req_id,
1374
+ store_options=store_options,
1375
+ )
1287
1376
  return token_response
1288
1377
 
1289
1378
  except Exception as e:
@@ -2243,10 +2332,45 @@ class ServerClient(Generic[TStoreOptions]):
2243
2332
  https://datatracker.ietf.org/doc/html/rfc8693
2244
2333
  """
2245
2334
  try:
2246
- # Validate options (Pydantic handles this automatically)
2247
2335
  if not isinstance(options, CustomTokenExchangeOptions):
2248
2336
  options = CustomTokenExchangeOptions(**options)
2249
2337
 
2338
+ if not options.subject_token.strip():
2339
+ raise CustomTokenExchangeError(
2340
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2341
+ "subject_token cannot be empty or whitespace-only"
2342
+ )
2343
+ if not options.subject_token_type.strip():
2344
+ raise CustomTokenExchangeError(
2345
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2346
+ "subject_token_type cannot be empty or whitespace-only"
2347
+ )
2348
+ if options.subject_token.strip().startswith("Bearer "):
2349
+ raise CustomTokenExchangeError(
2350
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2351
+ "subject_token should not include 'Bearer ' prefix"
2352
+ )
2353
+ if options.actor_token is not None and not options.actor_token.strip():
2354
+ raise CustomTokenExchangeError(
2355
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2356
+ "actor_token cannot be empty or whitespace-only"
2357
+ )
2358
+ if options.actor_token and options.actor_token.strip().startswith("Bearer "):
2359
+ raise CustomTokenExchangeError(
2360
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2361
+ "actor_token should not include 'Bearer ' prefix"
2362
+ )
2363
+ if options.actor_token and not options.actor_token_type:
2364
+ raise CustomTokenExchangeError(
2365
+ CustomTokenExchangeErrorCode.MISSING_ACTOR_TOKEN_TYPE,
2366
+ "actor_token_type is required when actor_token is provided"
2367
+ )
2368
+ if options.actor_token_type and not options.actor_token:
2369
+ raise CustomTokenExchangeError(
2370
+ CustomTokenExchangeErrorCode.MISSING_ACTOR_TOKEN,
2371
+ "actor_token is required when actor_token_type is provided"
2372
+ )
2373
+
2250
2374
  # Resolve domain
2251
2375
  domain = await self._resolve_current_domain(store_options)
2252
2376
  metadata = await self._get_oidc_metadata_cached(domain)
@@ -2309,8 +2433,25 @@ class ServerClient(Generic[TStoreOptions]):
2309
2433
  "Failed to parse token response as JSON"
2310
2434
  )
2311
2435
 
2312
- # Validate and return response
2313
- return TokenExchangeResponse(**token_data)
2436
+ token_response = TokenExchangeResponse(**token_data)
2437
+
2438
+ # Surface the actor claim for delegation exchanges. Best-effort:
2439
+ # a decode/verify hiccup must not fail an exchange the token
2440
+ # endpoint already accepted, so act stays None on any failure.
2441
+ if options.actor_token and token_response.id_token:
2442
+ try:
2443
+ jwks = await self._get_jwks_cached(domain, metadata)
2444
+ claims = await self._verify_and_decode_jwt(
2445
+ token_response.id_token, jwks, audience=self._client_id
2446
+ )
2447
+ # Apply the same normalized issuer check the login path uses
2448
+ # before trusting any claim from the token.
2449
+ if self._normalize_url(claims.get("iss", "")) == self._normalize_url(metadata.get("issuer")):
2450
+ token_response.act = claims.get("act")
2451
+ except Exception:
2452
+ token_response.act = None
2453
+
2454
+ return token_response
2314
2455
 
2315
2456
  except ValidationError as e:
2316
2457
  raise CustomTokenExchangeError(
@@ -2386,6 +2527,7 @@ class ServerClient(Generic[TStoreOptions]):
2386
2527
  # Extract user claims from ID token if present
2387
2528
  user_claims = None
2388
2529
  sid = PKCE.generate_random_string(32) # Default sid
2530
+
2389
2531
  if token_response.id_token:
2390
2532
  # Fetch JWKS and verify ID token signature
2391
2533
  jwks = await self._get_jwks_cached(domain, metadata)
@@ -2402,6 +2544,8 @@ class ServerClient(Generic[TStoreOptions]):
2402
2544
  "ID token issuer mismatch. Ensure your Auth0 domain is configured correctly."
2403
2545
  )
2404
2546
 
2547
+ # UserClaims allows extra fields, so any act claim in the
2548
+ # verified id_token is carried onto the session user here.
2405
2549
  user_claims = UserClaims.parse_obj(claims)
2406
2550
  # Extract sid from token if available
2407
2551
  sid = claims.get("sid", sid)
@@ -2467,7 +2611,7 @@ class ServerClient(Generic[TStoreOptions]):
2467
2611
  return result
2468
2612
 
2469
2613
  except Exception as e:
2470
- if isinstance(e, (CustomTokenExchangeError, ApiError)):
2614
+ if isinstance(e, (CustomTokenExchangeError, ApiError, IssuerValidationError)):
2471
2615
  raise
2472
2616
  raise CustomTokenExchangeError(
2473
2617
  CustomTokenExchangeErrorCode.TOKEN_EXCHANGE_FAILED,
@@ -5,7 +5,10 @@ These Pydantic models provide type safety and validation for all SDK data struct
5
5
 
6
6
  from typing import Any, Literal, Optional, Union
7
7
 
8
- from pydantic import BaseModel, Field, field_validator, model_validator
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+ # Upper bound (Unix seconds) for a plausible session_expiry
11
+ SESSION_EXPIRY_MAX_PLAUSIBLE = 10_000_000_000
9
12
 
10
13
 
11
14
  class UserClaims(BaseModel):
@@ -22,10 +25,22 @@ class UserClaims(BaseModel):
22
25
  email: Optional[str] = None
23
26
  email_verified: Optional[bool] = None
24
27
  org_id: Optional[str] = None
28
+ org_name: Optional[str] = None
29
+ # IPSIE SL1 claim: upstream IdP-asserted RP session ceiling (Unix seconds).
30
+ session_expiry: Optional[int] = None
25
31
 
26
32
  class Config:
27
33
  extra = "allow" # Allow additional fields not defined in the model
28
34
 
35
+ @field_validator('session_expiry', mode='before')
36
+ @classmethod
37
+ def _sanitize_session_expiry(cls, value: Any) -> Optional[int]:
38
+ if isinstance(value, bool) or not isinstance(value, int):
39
+ return None
40
+ if value <= 0 or value >= SESSION_EXPIRY_MAX_PLAUSIBLE:
41
+ return None
42
+ return value
43
+
29
44
 
30
45
  class TokenSet(BaseModel):
31
46
  """
@@ -54,6 +69,10 @@ class InternalStateData(BaseModel):
54
69
  """
55
70
  sid: str
56
71
  created_at: int
72
+ # IPSIE session_expiry ceiling (Unix seconds), stamped at session creation
73
+ # from the ID token's session_expiry claim. None when the upstream IdP did
74
+ # not assert one — in which case existing session behavior is unchanged.
75
+ session_expires_at: Optional[int] = None
57
76
 
58
77
 
59
78
  class SessionData(BaseModel):
@@ -91,6 +110,7 @@ class TransactionData(BaseModel):
91
110
  auth_session: Optional[str] = None
92
111
  redirect_uri: Optional[str] = None
93
112
  domain: Optional[str] = None
113
+ organization: Optional[str] = None
94
114
 
95
115
  class Config:
96
116
  extra = "allow" # Allow additional fields not defined in the model
@@ -128,6 +148,7 @@ class ServerClientOptionsBase(BaseModel):
128
148
  transaction_identifier: Optional[str] = "_a0_tx"
129
149
  state_identifier: Optional[str] = "_a0_session"
130
150
  custom_fetch: Optional[Any] = None # Function type hint would be more complex
151
+ organization: Optional[str] = None
131
152
 
132
153
 
133
154
  class ServerClientOptionsWithSecret(ServerClientOptionsBase):
@@ -147,6 +168,8 @@ class StartInteractiveLoginOptions(BaseModel):
147
168
  pushed_authorization_requests: Optional[bool] = False
148
169
  app_state: Optional[Any] = None
149
170
  authorization_params: Optional[dict[str, Any]] = None
171
+ organization: Optional[str] = None
172
+ invitation: Optional[str] = None
150
173
 
151
174
 
152
175
  class LogoutOptions(BaseModel):
@@ -257,8 +280,8 @@ class CustomTokenExchangeOptions(BaseModel):
257
280
  organization: Organization identifier for the token exchange (optional)
258
281
  authorization_params: Additional OAuth parameters (optional)
259
282
  """
260
- subject_token: str = Field(..., min_length=1)
261
- subject_token_type: str = Field(..., min_length=1)
283
+ subject_token: str
284
+ subject_token_type: str
262
285
  audience: Optional[str] = None
263
286
  scope: Optional[str] = None
264
287
  actor_token: Optional[str] = None
@@ -266,24 +289,6 @@ class CustomTokenExchangeOptions(BaseModel):
266
289
  organization: Optional[str] = None
267
290
  authorization_params: Optional[dict[str, Any]] = None
268
291
 
269
- @field_validator('subject_token', 'actor_token')
270
- @classmethod
271
- def validate_token_format(cls, v: Optional[str]) -> Optional[str]:
272
- """Validate token doesn't have Bearer prefix and isn't whitespace-only."""
273
- if v is not None:
274
- if not v.strip():
275
- raise ValueError("Token cannot be empty or whitespace-only")
276
- if v.strip().startswith("Bearer "):
277
- raise ValueError("Token should not include 'Bearer ' prefix")
278
- return v
279
-
280
- @model_validator(mode='after')
281
- def validate_actor_token_type(self) -> 'CustomTokenExchangeOptions':
282
- """Ensure actor_token_type is provided if actor_token is present."""
283
- if self.actor_token and not self.actor_token_type:
284
- raise ValueError("actor_token_type is required when actor_token is provided")
285
- return self
286
-
287
292
 
288
293
  class TokenExchangeResponse(BaseModel):
289
294
  """
@@ -297,6 +302,7 @@ class TokenExchangeResponse(BaseModel):
297
302
  issued_token_type: Format of issued token
298
303
  id_token: OpenID Connect ID token (optional)
299
304
  refresh_token: Refresh token (optional)
305
+ act: Actor claim for delegation/impersonation exchanges (optional)
300
306
  """
301
307
  access_token: str
302
308
  token_type: str = "Bearer"
@@ -305,6 +311,7 @@ class TokenExchangeResponse(BaseModel):
305
311
  issued_token_type: Optional[str] = None
306
312
  id_token: Optional[str] = None
307
313
  refresh_token: Optional[str] = None
314
+ act: Optional[dict[str, Any]] = None
308
315
 
309
316
 
310
317
  class LoginWithCustomTokenExchangeOptions(BaseModel):
@@ -313,8 +320,8 @@ class LoginWithCustomTokenExchangeOptions(BaseModel):
313
320
 
314
321
  Combines token exchange parameters with session management.
315
322
  """
316
- subject_token: str = Field(..., min_length=1)
317
- subject_token_type: str = Field(..., min_length=1)
323
+ subject_token: str
324
+ subject_token_type: str
318
325
  audience: Optional[str] = None
319
326
  scope: Optional[str] = None
320
327
  actor_token: Optional[str] = None
@@ -322,24 +329,6 @@ class LoginWithCustomTokenExchangeOptions(BaseModel):
322
329
  organization: Optional[str] = None
323
330
  authorization_params: Optional[dict[str, Any]] = None
324
331
 
325
- @field_validator('subject_token', 'actor_token')
326
- @classmethod
327
- def validate_token_format(cls, v: Optional[str]) -> Optional[str]:
328
- """Validate token doesn't have Bearer prefix and isn't whitespace-only."""
329
- if v is not None:
330
- if not v.strip():
331
- raise ValueError("Token cannot be empty or whitespace-only")
332
- if v.strip().startswith("Bearer "):
333
- raise ValueError("Token should not include 'Bearer ' prefix")
334
- return v
335
-
336
- @model_validator(mode='after')
337
- def validate_actor_token_type(self) -> 'LoginWithCustomTokenExchangeOptions':
338
- """Ensure actor_token_type is provided if actor_token is present."""
339
- if self.actor_token and not self.actor_token_type:
340
- raise ValueError("actor_token_type is required when actor_token is provided")
341
- return self
342
-
343
332
 
344
333
  class LoginWithCustomTokenExchangeResult(BaseModel):
345
334
  """
@@ -198,6 +198,19 @@ class AccessTokenErrorCode:
198
198
  INCORRECT_AUDIENCE = "incorrect_audience"
199
199
  MISSING_SESSION_DOMAIN = "missing_session_domain"
200
200
  DOMAIN_MISMATCH = "domain_mismatch"
201
+ SESSION_EXPIRED = "session_expired"
202
+
203
+
204
+ class OrganizationTokenValidationError(Auth0Error):
205
+ """
206
+ Raised when org_id or org_name claim in the ID token fails validation
207
+ against the organization value that was requested at login.
208
+ """
209
+ code = "organization_token_validation_error"
210
+
211
+ def __init__(self, message: str):
212
+ super().__init__(message)
213
+ self.name = "OrganizationTokenValidationError"
201
214
 
202
215
 
203
216
  class AccessTokenForConnectionErrorCode:
@@ -210,6 +223,19 @@ class AccessTokenForConnectionErrorCode:
210
223
  DOMAIN_MISMATCH = "domain_mismatch"
211
224
 
212
225
 
226
+ class SessionExpiredError(Auth0Error):
227
+ """
228
+ Error raised when a session is rejected at login because its
229
+ session_expiry ceiling is already in the past.
230
+ """
231
+ code = AccessTokenErrorCode.SESSION_EXPIRED
232
+
233
+ def __init__(self, message: Optional[str] = None, cause=None):
234
+ super().__init__(message or "The session has expired and the user must re-authenticate.")
235
+ self.name = "SessionExpiredError"
236
+ self.cause = cause
237
+
238
+
213
239
  class CustomTokenExchangeError(Auth0Error):
214
240
  """
215
241
  Error raised during custom token exchange operations.
@@ -225,6 +251,7 @@ class CustomTokenExchangeErrorCode:
225
251
  """Error codes for custom token exchange operations."""
226
252
  INVALID_TOKEN_FORMAT = "invalid_token_format"
227
253
  MISSING_ACTOR_TOKEN_TYPE = "missing_actor_token_type"
254
+ MISSING_ACTOR_TOKEN = "missing_actor_token"
228
255
  TOKEN_EXCHANGE_FAILED = "token_exchange_failed"
229
256
  INVALID_RESPONSE = "invalid_response"
230
257