Skip to content

Commit 4c1a173

Browse files
committed
chore: renaming of classes to bring consistency, adding fallback with validations for options params
1 parent a2a0afe commit 4c1a173

5 files changed

Lines changed: 131 additions & 53 deletions

File tree

examples/AnonymousSessions.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,14 @@ All anonymous session errors subclass `AnonymousApiError`, carrying a `.code` yo
122122

123123
```python
124124
from auth0_server_python.error import (
125-
AnonymousFeatureNotEnabledError, # tenant flag is off
126-
AnonymousClientNotEnabledError, # client not enabled for anonymous sessions
127-
AnonymousClientNotSupportedError, # e.g. a DPoP-mandated client — see Known Limitations
128-
AnonymousResourceServerError, # audience not a valid/enabled resource server
129-
AnonymousScopeError, # scope not granted to anonymous callers
130-
AnonymousSessionCreateError, # base class for create/re-mint failures
131-
AnonymousTokenError, # get_token() failure with no active session
132-
AnonymousSessionIntrospectError,
125+
AnonymousFeatureNotEnabledError,
126+
AnonymousClientNotEnabledError,
127+
AnonymousClientNotSupportedError,
128+
AnonymousResourceServerError,
129+
AnonymousScopeError,
130+
AnonymousCreateError,
131+
AnonymousTokenError,
132+
AnonymousIntrospectError,
133133
AnonymousLogoutError,
134134
)
135135

src/auth0_server_python/auth_server/anonymous_client.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import json
77
import time
8-
from typing import Any, Optional
8+
from typing import Any, Optional, Union
99

1010
import httpx
1111
from pydantic import ValidationError
@@ -16,18 +16,19 @@
1616
AnonymousSessionContext,
1717
AnonymousSessionIntrospection,
1818
AnonymousTokenResponse,
19+
CreateAnonymousSessionOptions,
1920
)
2021
from auth0_server_python.encryption.encrypt import decrypt, encrypt
2122
from auth0_server_python.error import (
2223
AnonymousApiError,
2324
AnonymousClientNotEnabledError,
2425
AnonymousClientNotSupportedError,
26+
AnonymousCreateError,
2527
AnonymousFeatureNotEnabledError,
28+
AnonymousIntrospectError,
2629
AnonymousLogoutError,
2730
AnonymousResourceServerError,
2831
AnonymousScopeError,
29-
AnonymousSessionCreateError,
30-
AnonymousSessionIntrospectError,
3132
AnonymousTokenError,
3233
ConfigurationError,
3334
DomainResolverError,
@@ -211,13 +212,13 @@ def _map_anonymous_error(
211212
return AnonymousScopeError(description, error_data)
212213

213214
if operation == "create":
214-
return AnonymousSessionCreateError(description, cause=error_data)
215+
return AnonymousCreateError(description, cause=error_data)
215216
if operation == "token":
216217
return AnonymousTokenError(description, error_data)
217218
if operation == "logout":
218219
return AnonymousLogoutError(description, error_data)
219220
if operation == "introspect":
220-
return AnonymousSessionIntrospectError(description, error_data)
221+
return AnonymousIntrospectError(description, error_data)
221222
return AnonymousApiError(code or "anonymous_error", description, error_data)
222223

223224
# ============================================================================
@@ -232,25 +233,25 @@ def _validate_metadata(metadata: Optional[dict[str, Any]]) -> None:
232233
metadata: The metadata dict to validate, or None.
233234
234235
Raises:
235-
AnonymousSessionCreateError: metadata is not a dict, contains a
236+
AnonymousCreateError: metadata is not a dict, contains a
236237
disallowed key, a non-string value, or exceeds 1KB.
237238
"""
238239
if metadata is None:
239240
return
240241
if not isinstance(metadata, dict):
241-
raise AnonymousSessionCreateError("metadata must be a JSON object", code="invalid_metadata")
242+
raise AnonymousCreateError("metadata must be a JSON object", code="invalid_metadata")
242243
for key, value in metadata.items():
243244
if key in _DANGEROUS_METADATA_KEYS:
244-
raise AnonymousSessionCreateError(
245+
raise AnonymousCreateError(
245246
f"metadata key '{key}' is not allowed", code="invalid_metadata"
246247
)
247248
if not isinstance(value, str):
248-
raise AnonymousSessionCreateError(
249+
raise AnonymousCreateError(
249250
f"metadata value for key '{key}' must be a string", code="invalid_metadata"
250251
)
251252
size = len(json.dumps(metadata).encode("utf-8"))
252253
if size > _METADATA_MAX_BYTES:
253-
raise AnonymousSessionCreateError(
254+
raise AnonymousCreateError(
254255
"metadata exceeds the 1KB size limit", code="metadata_too_large"
255256
)
256257

@@ -325,7 +326,7 @@ async def _create_session_at(
325326
The newly created AnonymousSession, with is_new=True.
326327
327328
Raises:
328-
AnonymousSessionCreateError: The request failed, or the response
329+
AnonymousCreateError: The request failed, or the response
329330
was invalid or missing required fields.
330331
"""
331332
base_url = f"https://{domain}"
@@ -343,7 +344,7 @@ async def _create_session_at(
343344
try:
344345
response = await client.post(f"{base_url}/anonymous/token", json=body)
345346
except httpx.HTTPError as e:
346-
raise AnonymousSessionCreateError(
347+
raise AnonymousCreateError(
347348
"Failed to reach the anonymous token endpoint"
348349
) from e
349350

@@ -352,18 +353,18 @@ async def _create_session_at(
352353
mapped = self._map_anonymous_error(response.status_code, error_data, "create")
353354
if isinstance(mapped, _AnonymousSessionExpired):
354355
# Internal-only type must never escape.
355-
raise AnonymousSessionCreateError(str(mapped))
356+
raise AnonymousCreateError(str(mapped))
356357
raise mapped
357358

358359
try:
359360
token_response = AnonymousTokenResponse.model_validate(response.json())
360361
except (json.JSONDecodeError, ValueError, ValidationError) as e:
361-
raise AnonymousSessionCreateError(
362+
raise AnonymousCreateError(
362363
"Failed to parse anonymous token response"
363364
) from e
364365

365366
if not token_response.session_token:
366-
raise AnonymousSessionCreateError("Anonymous token response missing required fields")
367+
raise AnonymousCreateError("Anonymous token response missing required fields")
367368

368369
now = int(time.time())
369370
context = AnonymousSessionContext(
@@ -527,6 +528,7 @@ async def get_session_token_for_injection(
527528

528529
async def create_session(
529530
self,
531+
options: Optional[Union[CreateAnonymousSessionOptions, dict[str, Any]]] = None,
530532
*,
531533
audience: Optional[str] = None,
532534
scope: Optional[str] = None,
@@ -536,22 +538,37 @@ async def create_session(
536538
"""Mint a fresh anon@<uuid> identity.
537539
538540
Args:
539-
audience: Audience for the session. Falls back to the client's
540-
configured default when omitted.
541-
scope: Scope for the session. Falls back to the client's
542-
configured default when omitted.
541+
options: Optional bundle of audience/scope/metadata, accepted as a
542+
CreateAnonymousSessionOptions or a plain dict. Explicit keyword
543+
arguments below always win over the same field on options.
544+
audience: Audience for the session. Falls back to options.audience,
545+
then to the client's configured default, when omitted.
546+
scope: Scope for the session. Falls back to options.scope, then to
547+
the client's configured default, when omitted.
543548
metadata: Metadata to attach at creation, up to 1KB. Cannot be
544-
changed after creation.
549+
changed after creation. Falls back to options.metadata.
545550
store_options: Options passed to the anonymous store.
546551
547552
Returns:
548553
The newly created AnonymousSession.
549554
550555
Raises:
551556
ConfigurationError: No anonymous_store configured.
552-
AnonymousSessionCreateError: Local validation or server rejection.
557+
AnonymousCreateError: Invalid options, local validation
558+
failure, or server rejection.
553559
"""
554560
self._require_store()
561+
if options is not None:
562+
if isinstance(options, dict):
563+
try:
564+
options = CreateAnonymousSessionOptions(**options)
565+
except ValidationError as e:
566+
raise AnonymousCreateError(
567+
"Invalid create_session options", code="invalid_options"
568+
) from e
569+
audience = audience if audience is not None else options.audience
570+
scope = scope if scope is not None else options.scope
571+
metadata = metadata if metadata is not None else options.metadata
555572
self._validate_metadata(metadata)
556573
audience = audience or self._default_audience
557574
scope = scope or self._default_scope
@@ -645,18 +662,18 @@ async def introspect(
645662
646663
Raises:
647664
ConfigurationError: No anonymous_store configured.
648-
AnonymousSessionIntrospectError: No active session, or a request
665+
AnonymousIntrospectError: No active session, or a request
649666
failure.
650667
"""
651668
self._require_store()
652669
stored = await self._anonymous_store.get(ANON_IDENTIFIER, options=store_options)
653670
if not stored:
654-
raise AnonymousSessionIntrospectError("No active anonymous session to introspect.")
671+
raise AnonymousIntrospectError("No active anonymous session to introspect.")
655672

656673
try:
657674
context = self._decrypt_context(stored)
658675
except _AnonymousSessionExpired as e:
659-
raise AnonymousSessionIntrospectError(
676+
raise AnonymousIntrospectError(
660677
"Stored anonymous session is invalid or corrupted."
661678
) from e
662679

@@ -670,21 +687,21 @@ async def introspect(
670687
auth=BearerAuth(context.access_token),
671688
)
672689
except httpx.HTTPError as e:
673-
raise AnonymousSessionIntrospectError(
690+
raise AnonymousIntrospectError(
674691
"Failed to reach the anonymous userinfo endpoint"
675692
) from e
676693

677694
if response.status_code != 200:
678695
error_data = self._parse_anonymous_error_body(response)
679696
mapped = self._map_anonymous_error(response.status_code, error_data, "introspect")
680697
if isinstance(mapped, _AnonymousSessionExpired):
681-
raise AnonymousSessionIntrospectError(str(mapped))
698+
raise AnonymousIntrospectError(str(mapped))
682699
raise mapped
683700

684701
try:
685702
return AnonymousSessionIntrospection.model_validate(response.json())
686703
except (json.JSONDecodeError, ValueError, ValidationError) as e:
687-
raise AnonymousSessionIntrospectError(
704+
raise AnonymousIntrospectError(
688705
"Failed to parse anonymous introspection response"
689706
) from e
690707

src/auth0_server_python/auth_types/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ class LogoutOptions(BaseModel):
222222
return_to: Optional[str] = None
223223

224224

225+
class CreateAnonymousSessionOptions(BaseModel):
226+
"""Options bundle for create_session(): audience, scope, and metadata."""
227+
228+
model_config = ConfigDict(extra="forbid")
229+
230+
audience: Optional[str] = None
231+
scope: Optional[str] = None
232+
metadata: Optional[dict[str, Any]] = None
233+
234+
225235
class AuthorizationParameters(BaseModel):
226236
"""
227237
Parameters used in authorization requests.

src/auth0_server_python/error/__init__.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,10 @@ def __init__(
382382
self.cause = cause
383383

384384

385-
class AnonymousSessionCreateError(AnonymousApiError):
385+
class AnonymousCreateError(AnonymousApiError):
386386
"""Error thrown when creating or re-minting an anonymous session fails."""
387387

388-
def __init__(self, message: str, code: str = "anonymous_session_create_error", cause: Optional[dict] = None):
388+
def __init__(self, message: str, code: str = "anonymous_create_error", cause: Optional[dict] = None):
389389
super().__init__(code, message, cause)
390390

391391

@@ -403,7 +403,7 @@ def __init__(self, message: str, cause: Optional[dict] = None):
403403
super().__init__("anonymous_token_error", message, cause)
404404

405405

406-
class AnonymousSessionIntrospectError(AnonymousApiError):
406+
class AnonymousIntrospectError(AnonymousApiError):
407407
"""
408408
Error thrown when introspect() fails.
409409
@@ -412,38 +412,38 @@ class AnonymousSessionIntrospectError(AnonymousApiError):
412412
"""
413413

414414
def __init__(self, message: str, cause: Optional[dict] = None):
415-
super().__init__("anonymous_session_introspect_error", message, cause)
415+
super().__init__("anonymous_introspect_error", message, cause)
416416

417417

418-
class AnonymousFeatureNotEnabledError(AnonymousSessionCreateError):
418+
class AnonymousFeatureNotEnabledError(AnonymousCreateError):
419419
"""Error thrown when the tenant has not enabled the anonymous sessions add-on."""
420420

421421
def __init__(self, message: str, cause: Optional[dict] = None):
422422
super().__init__(message, "anonymous_feature_not_enabled_error", cause)
423423

424424

425-
class AnonymousClientNotEnabledError(AnonymousSessionCreateError):
425+
class AnonymousClientNotEnabledError(AnonymousCreateError):
426426
"""Error thrown when the client is not enabled for anonymous sessions."""
427427

428428
def __init__(self, message: str, cause: Optional[dict] = None):
429429
super().__init__(message, "anonymous_client_not_enabled_error", cause)
430430

431431

432-
class AnonymousClientNotSupportedError(AnonymousSessionCreateError):
432+
class AnonymousClientNotSupportedError(AnonymousCreateError):
433433
"""Error thrown when the client type does not support anonymous sessions (e.g. DPoP-mandated)."""
434434

435435
def __init__(self, message: str, cause: Optional[dict] = None):
436436
super().__init__(message, "anonymous_client_not_supported_error", cause)
437437

438438

439-
class AnonymousResourceServerError(AnonymousSessionCreateError):
439+
class AnonymousResourceServerError(AnonymousCreateError):
440440
"""Error thrown when the requested audience is not a valid resource server."""
441441

442442
def __init__(self, message: str, cause: Optional[dict] = None):
443443
super().__init__(message, "anonymous_resource_server_error", cause)
444444

445445

446-
class AnonymousScopeError(AnonymousSessionCreateError):
446+
class AnonymousScopeError(AnonymousCreateError):
447447
"""Error thrown when the requested scope is not granted to anonymous callers."""
448448

449449
def __init__(self, message: str, cause: Optional[dict] = None):

0 commit comments

Comments
 (0)