auth0-server-python 1.0.0b10__tar.gz → 1.0.0b11__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.0b11}/PKG-INFO +5 -1
  2. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/README.md +4 -0
  3. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/pyproject.toml +1 -1
  4. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_server/server_client.py +104 -5
  5. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_types/__init__.py +12 -41
  6. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/error/__init__.py +13 -0
  7. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/tests/test_server_client.py +1342 -35
  8. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/utils/helpers.py +42 -1
  9. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/LICENSE +0 -0
  10. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
  11. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
  12. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_server/__init__.py +0 -0
  13. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_server/mfa_client.py +0 -0
  14. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/auth_server/my_account_client.py +0 -0
  15. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/encryption/__init__.py +0 -0
  16. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/encryption/encrypt.py +0 -0
  17. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/store/__init__.py +0 -0
  18. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/store/abstract.py +0 -0
  19. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/telemetry.py +0 -0
  20. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/tests/test_mfa_client.py +0 -0
  21. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/tests/test_my_account_client.py +0 -0
  22. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/src/auth0_server_python/tests/test_telemetry.py +0 -0
  23. {auth0_server_python-1.0.0b10 → auth0_server_python-1.0.0b11}/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.0b11
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):
@@ -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):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0b10"
3
+ version = "1.0.0b11"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -54,6 +54,7 @@ from auth0_server_python.error import (
54
54
  MfaRequiredError,
55
55
  MissingRequiredArgumentError,
56
56
  MissingTransactionError,
57
+ OrganizationTokenValidationError,
57
58
  PollingApiError,
58
59
  StartLinkUserError,
59
60
  )
@@ -61,6 +62,7 @@ from auth0_server_python.telemetry import Telemetry
61
62
  from auth0_server_python.utils import PKCE, URL, State
62
63
  from auth0_server_python.utils.helpers import (
63
64
  build_domain_resolver_context,
65
+ validate_org_claims,
64
66
  validate_resolved_domain_value,
65
67
  )
66
68
 
@@ -96,6 +98,7 @@ class ServerClient(Generic[TStoreOptions]):
96
98
  state_identifier: str = "_a0_session",
97
99
  authorization_params: Optional[dict[str, Any]] = None,
98
100
  pushed_authorization_requests: bool = False,
101
+ organization: Optional[str] = None,
99
102
  ):
100
103
  """
101
104
  Initialize the Auth0 server client.
@@ -112,6 +115,9 @@ class ServerClient(Generic[TStoreOptions]):
112
115
  state_identifier: Identifier for state data
113
116
  authorization_params: Default parameters for authorization requests
114
117
  pushed_authorization_requests: Whether to use Pushed Authorization Requests
118
+ organization: Default organization for all login flows from this client.
119
+ Can be an org ID (e.g. 'org_abc123') or an org name (e.g. 'acme-corp').
120
+ Per-login values passed in StartInteractiveLoginOptions always override this.
115
121
  """
116
122
  if not secret:
117
123
  raise MissingRequiredArgumentError("secret")
@@ -146,6 +152,7 @@ class ServerClient(Generic[TStoreOptions]):
146
152
  self._secret = secret
147
153
  self._default_authorization_params = authorization_params or {}
148
154
  self._pushed_authorization_requests = pushed_authorization_requests # store the flag
155
+ self._organization = organization
149
156
 
150
157
  # Initialize stores
151
158
  self._transaction_store = transaction_store
@@ -207,6 +214,7 @@ class ServerClient(Generic[TStoreOptions]):
207
214
 
208
215
  return value.rstrip('/')
209
216
 
217
+
210
218
  async def _resolve_current_domain(self, store_options=None) -> str:
211
219
  """Resolve domain from resolver function or return static domain."""
212
220
  if self._domain_resolver:
@@ -502,6 +510,16 @@ class ServerClient(Generic[TStoreOptions]):
502
510
  merged_scope = self._merge_scope_with_defaults(requested_scope, audience)
503
511
  auth_params["scope"] = merged_scope
504
512
 
513
+ # Typed org/invitation fields win over anything already in auth_params from authorization_params.
514
+ resolved_org = options.organization or self._organization
515
+ if resolved_org and not resolved_org.strip():
516
+ raise InvalidArgumentError("organization", "organization must not be blank")
517
+ if resolved_org:
518
+ auth_params["organization"] = resolved_org
519
+
520
+ if options.invitation:
521
+ auth_params["invitation"] = options.invitation
522
+
505
523
  # Build the transaction data to store with domain
506
524
  transaction_data = TransactionData(
507
525
  code_verifier=code_verifier,
@@ -509,6 +527,7 @@ class ServerClient(Generic[TStoreOptions]):
509
527
  audience=audience,
510
528
  domain=origin_domain,
511
529
  redirect_uri=auth_params.get("redirect_uri"),
530
+ organization=resolved_org,
512
531
  )
513
532
 
514
533
  # Store the transaction data
@@ -638,8 +657,26 @@ class ServerClient(Generic[TStoreOptions]):
638
657
  user_info = token_response.get("userinfo")
639
658
  user_claims = None
640
659
  id_token = token_response.get("id_token")
660
+ expected_org = transaction_data.organization
661
+
662
+ if not user_info and not id_token and expected_org:
663
+ raise OrganizationTokenValidationError(
664
+ "Organization was requested but the token response included neither an ID token nor userinfo; "
665
+ "cannot verify organization membership"
666
+ )
641
667
 
642
668
  if user_info:
669
+ if not isinstance(user_info, dict):
670
+ if expected_org:
671
+ raise OrganizationTokenValidationError(
672
+ "Userinfo response is not a valid claims dictionary; cannot verify organization membership"
673
+ )
674
+ raise ApiError(
675
+ "invalid_response",
676
+ "Userinfo response is not a valid claims dictionary"
677
+ )
678
+ if expected_org:
679
+ validate_org_claims(user_info, expected_org)
643
680
  user_claims = UserClaims.parse_obj(user_info)
644
681
  elif id_token:
645
682
  # Fetch JWKS for signature verification
@@ -656,6 +693,10 @@ class ServerClient(Generic[TStoreOptions]):
656
693
  if self._normalize_url(token_issuer) != self._normalize_url(origin_issuer):
657
694
  raise IssuerValidationError("ID token issuer mismatch. Ensure your Auth0 domain is configured correctly.")
658
695
 
696
+ # Organization claim validation — mandatory when org was requested.
697
+ if expected_org:
698
+ validate_org_claims(claims, expected_org)
699
+
659
700
  user_claims = UserClaims.parse_obj(claims)
660
701
  except ValueError as e:
661
702
  raise ApiError("jwks_key_not_found", str(e))
@@ -1283,7 +1324,10 @@ class ServerClient(Generic[TStoreOptions]):
1283
1324
  while time.time() < end_time:
1284
1325
  # Make token request
1285
1326
  try:
1286
- token_response = await self.backchannel_authentication_grant(auth_req_id, store_options=store_options)
1327
+ token_response = await self.backchannel_authentication_grant(
1328
+ auth_req_id,
1329
+ store_options=store_options,
1330
+ )
1287
1331
  return token_response
1288
1332
 
1289
1333
  except Exception as e:
@@ -2243,10 +2287,45 @@ class ServerClient(Generic[TStoreOptions]):
2243
2287
  https://datatracker.ietf.org/doc/html/rfc8693
2244
2288
  """
2245
2289
  try:
2246
- # Validate options (Pydantic handles this automatically)
2247
2290
  if not isinstance(options, CustomTokenExchangeOptions):
2248
2291
  options = CustomTokenExchangeOptions(**options)
2249
2292
 
2293
+ if not options.subject_token.strip():
2294
+ raise CustomTokenExchangeError(
2295
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2296
+ "subject_token cannot be empty or whitespace-only"
2297
+ )
2298
+ if not options.subject_token_type.strip():
2299
+ raise CustomTokenExchangeError(
2300
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2301
+ "subject_token_type cannot be empty or whitespace-only"
2302
+ )
2303
+ if options.subject_token.strip().startswith("Bearer "):
2304
+ raise CustomTokenExchangeError(
2305
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2306
+ "subject_token should not include 'Bearer ' prefix"
2307
+ )
2308
+ if options.actor_token is not None and not options.actor_token.strip():
2309
+ raise CustomTokenExchangeError(
2310
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2311
+ "actor_token cannot be empty or whitespace-only"
2312
+ )
2313
+ if options.actor_token and options.actor_token.strip().startswith("Bearer "):
2314
+ raise CustomTokenExchangeError(
2315
+ CustomTokenExchangeErrorCode.INVALID_TOKEN_FORMAT,
2316
+ "actor_token should not include 'Bearer ' prefix"
2317
+ )
2318
+ if options.actor_token and not options.actor_token_type:
2319
+ raise CustomTokenExchangeError(
2320
+ CustomTokenExchangeErrorCode.MISSING_ACTOR_TOKEN_TYPE,
2321
+ "actor_token_type is required when actor_token is provided"
2322
+ )
2323
+ if options.actor_token_type and not options.actor_token:
2324
+ raise CustomTokenExchangeError(
2325
+ CustomTokenExchangeErrorCode.MISSING_ACTOR_TOKEN,
2326
+ "actor_token is required when actor_token_type is provided"
2327
+ )
2328
+
2250
2329
  # Resolve domain
2251
2330
  domain = await self._resolve_current_domain(store_options)
2252
2331
  metadata = await self._get_oidc_metadata_cached(domain)
@@ -2309,8 +2388,25 @@ class ServerClient(Generic[TStoreOptions]):
2309
2388
  "Failed to parse token response as JSON"
2310
2389
  )
2311
2390
 
2312
- # Validate and return response
2313
- return TokenExchangeResponse(**token_data)
2391
+ token_response = TokenExchangeResponse(**token_data)
2392
+
2393
+ # Surface the actor claim for delegation exchanges. Best-effort:
2394
+ # a decode/verify hiccup must not fail an exchange the token
2395
+ # endpoint already accepted, so act stays None on any failure.
2396
+ if options.actor_token and token_response.id_token:
2397
+ try:
2398
+ jwks = await self._get_jwks_cached(domain, metadata)
2399
+ claims = await self._verify_and_decode_jwt(
2400
+ token_response.id_token, jwks, audience=self._client_id
2401
+ )
2402
+ # Apply the same normalized issuer check the login path uses
2403
+ # before trusting any claim from the token.
2404
+ if self._normalize_url(claims.get("iss", "")) == self._normalize_url(metadata.get("issuer")):
2405
+ token_response.act = claims.get("act")
2406
+ except Exception:
2407
+ token_response.act = None
2408
+
2409
+ return token_response
2314
2410
 
2315
2411
  except ValidationError as e:
2316
2412
  raise CustomTokenExchangeError(
@@ -2386,6 +2482,7 @@ class ServerClient(Generic[TStoreOptions]):
2386
2482
  # Extract user claims from ID token if present
2387
2483
  user_claims = None
2388
2484
  sid = PKCE.generate_random_string(32) # Default sid
2485
+
2389
2486
  if token_response.id_token:
2390
2487
  # Fetch JWKS and verify ID token signature
2391
2488
  jwks = await self._get_jwks_cached(domain, metadata)
@@ -2402,6 +2499,8 @@ class ServerClient(Generic[TStoreOptions]):
2402
2499
  "ID token issuer mismatch. Ensure your Auth0 domain is configured correctly."
2403
2500
  )
2404
2501
 
2502
+ # UserClaims allows extra fields, so any act claim in the
2503
+ # verified id_token is carried onto the session user here.
2405
2504
  user_claims = UserClaims.parse_obj(claims)
2406
2505
  # Extract sid from token if available
2407
2506
  sid = claims.get("sid", sid)
@@ -2467,7 +2566,7 @@ class ServerClient(Generic[TStoreOptions]):
2467
2566
  return result
2468
2567
 
2469
2568
  except Exception as e:
2470
- if isinstance(e, (CustomTokenExchangeError, ApiError)):
2569
+ if isinstance(e, (CustomTokenExchangeError, ApiError, IssuerValidationError)):
2471
2570
  raise
2472
2571
  raise CustomTokenExchangeError(
2473
2572
  CustomTokenExchangeErrorCode.TOKEN_EXCHANGE_FAILED,
@@ -5,7 +5,7 @@ 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
9
9
 
10
10
 
11
11
  class UserClaims(BaseModel):
@@ -22,6 +22,7 @@ class UserClaims(BaseModel):
22
22
  email: Optional[str] = None
23
23
  email_verified: Optional[bool] = None
24
24
  org_id: Optional[str] = None
25
+ org_name: Optional[str] = None
25
26
 
26
27
  class Config:
27
28
  extra = "allow" # Allow additional fields not defined in the model
@@ -91,6 +92,7 @@ class TransactionData(BaseModel):
91
92
  auth_session: Optional[str] = None
92
93
  redirect_uri: Optional[str] = None
93
94
  domain: Optional[str] = None
95
+ organization: Optional[str] = None
94
96
 
95
97
  class Config:
96
98
  extra = "allow" # Allow additional fields not defined in the model
@@ -128,6 +130,7 @@ class ServerClientOptionsBase(BaseModel):
128
130
  transaction_identifier: Optional[str] = "_a0_tx"
129
131
  state_identifier: Optional[str] = "_a0_session"
130
132
  custom_fetch: Optional[Any] = None # Function type hint would be more complex
133
+ organization: Optional[str] = None
131
134
 
132
135
 
133
136
  class ServerClientOptionsWithSecret(ServerClientOptionsBase):
@@ -147,6 +150,8 @@ class StartInteractiveLoginOptions(BaseModel):
147
150
  pushed_authorization_requests: Optional[bool] = False
148
151
  app_state: Optional[Any] = None
149
152
  authorization_params: Optional[dict[str, Any]] = None
153
+ organization: Optional[str] = None
154
+ invitation: Optional[str] = None
150
155
 
151
156
 
152
157
  class LogoutOptions(BaseModel):
@@ -257,8 +262,8 @@ class CustomTokenExchangeOptions(BaseModel):
257
262
  organization: Organization identifier for the token exchange (optional)
258
263
  authorization_params: Additional OAuth parameters (optional)
259
264
  """
260
- subject_token: str = Field(..., min_length=1)
261
- subject_token_type: str = Field(..., min_length=1)
265
+ subject_token: str
266
+ subject_token_type: str
262
267
  audience: Optional[str] = None
263
268
  scope: Optional[str] = None
264
269
  actor_token: Optional[str] = None
@@ -266,24 +271,6 @@ class CustomTokenExchangeOptions(BaseModel):
266
271
  organization: Optional[str] = None
267
272
  authorization_params: Optional[dict[str, Any]] = None
268
273
 
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
274
 
288
275
  class TokenExchangeResponse(BaseModel):
289
276
  """
@@ -297,6 +284,7 @@ class TokenExchangeResponse(BaseModel):
297
284
  issued_token_type: Format of issued token
298
285
  id_token: OpenID Connect ID token (optional)
299
286
  refresh_token: Refresh token (optional)
287
+ act: Actor claim for delegation/impersonation exchanges (optional)
300
288
  """
301
289
  access_token: str
302
290
  token_type: str = "Bearer"
@@ -305,6 +293,7 @@ class TokenExchangeResponse(BaseModel):
305
293
  issued_token_type: Optional[str] = None
306
294
  id_token: Optional[str] = None
307
295
  refresh_token: Optional[str] = None
296
+ act: Optional[dict[str, Any]] = None
308
297
 
309
298
 
310
299
  class LoginWithCustomTokenExchangeOptions(BaseModel):
@@ -313,8 +302,8 @@ class LoginWithCustomTokenExchangeOptions(BaseModel):
313
302
 
314
303
  Combines token exchange parameters with session management.
315
304
  """
316
- subject_token: str = Field(..., min_length=1)
317
- subject_token_type: str = Field(..., min_length=1)
305
+ subject_token: str
306
+ subject_token_type: str
318
307
  audience: Optional[str] = None
319
308
  scope: Optional[str] = None
320
309
  actor_token: Optional[str] = None
@@ -322,24 +311,6 @@ class LoginWithCustomTokenExchangeOptions(BaseModel):
322
311
  organization: Optional[str] = None
323
312
  authorization_params: Optional[dict[str, Any]] = None
324
313
 
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
314
 
344
315
  class LoginWithCustomTokenExchangeResult(BaseModel):
345
316
  """
@@ -200,6 +200,18 @@ class AccessTokenErrorCode:
200
200
  DOMAIN_MISMATCH = "domain_mismatch"
201
201
 
202
202
 
203
+ class OrganizationTokenValidationError(Auth0Error):
204
+ """
205
+ Raised when org_id or org_name claim in the ID token fails validation
206
+ against the organization value that was requested at login.
207
+ """
208
+ code = "organization_token_validation_error"
209
+
210
+ def __init__(self, message: str):
211
+ super().__init__(message)
212
+ self.name = "OrganizationTokenValidationError"
213
+
214
+
203
215
  class AccessTokenForConnectionErrorCode:
204
216
  """Error codes for connection-specific token operations."""
205
217
  MISSING_REFRESH_TOKEN = "missing_refresh_token"
@@ -225,6 +237,7 @@ class CustomTokenExchangeErrorCode:
225
237
  """Error codes for custom token exchange operations."""
226
238
  INVALID_TOKEN_FORMAT = "invalid_token_format"
227
239
  MISSING_ACTOR_TOKEN_TYPE = "missing_actor_token_type"
240
+ MISSING_ACTOR_TOKEN = "missing_actor_token"
228
241
  TOKEN_EXCHANGE_FAILED = "token_exchange_failed"
229
242
  INVALID_RESPONSE = "invalid_response"
230
243