diff --git a/EXAMPLES.md b/EXAMPLES.md index 9f431ac..c9a7a1b 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -273,3 +273,65 @@ async def verify_dpop_token(access_token, dpop_proof, http_method, http_url): "proof_claims": proof_claims } ``` + +## Anonymous Callers + +Anonymous Sessions give a visitor an Auth0 identity before they log in. The access token issued for an anonymous session is a standard Auth0 Bearer JWT, so this SDK validates it like any other token. The one difference is the `sub` claim, which starts with `anon@`. + +An anonymous token passes verification by default. To treat anonymous callers differently, or block them, check the `sub` claim after verifying the token. The SDK does not make that authorization decision for you. + +### Serve everyone, branch in the handler + +```python +import asyncio +from auth0_api_python import ApiClient, ApiClientOptions + +async def handle_cart(headers): + api_client = ApiClient(ApiClientOptions( + domain="your-tenant.auth0.com", + audience="https://api.example.com" + )) + + claims = await api_client.verify_request(headers=headers) + is_anonymous = claims.get("sub", "").startswith("anon@") + + if is_anonymous: + return {"cart": load_guest_cart(claims["sub"])} + return {"cart": load_user_cart(claims["sub"])} +``` + +### Block anonymous callers on a specific route + +```python +async def handle_checkout(headers): + api_client = ApiClient(ApiClientOptions( + domain="your-tenant.auth0.com", + audience="https://api.example.com" + )) + + claims = await api_client.verify_request(headers=headers) + if claims.get("sub", "").startswith("anon@"): + raise PermissionError("Anonymous callers are not allowed on this route") + + return {"order": create_order(claims["sub"])} +``` + +### Block anonymous callers everywhere + +The SDK has no global "reject anonymous" switch. Centralize the check in whatever shared layer your framework uses for auth (middleware, a FastAPI dependency, a decorator). + +```python +async def require_logged_in_user(headers): + api_client = ApiClient(ApiClientOptions( + domain="your-tenant.auth0.com", + audience="https://api.example.com" + )) + + claims = await api_client.verify_request(headers=headers) + if claims.get("sub", "").startswith("anon@"): + raise PermissionError("Anonymous callers are not allowed") + return claims +``` + +> [!NOTE] +> The `anon@` prefix on `sub` is the only signal that distinguishes an anonymous caller from a logged-in user. diff --git a/README.md b/README.md index 367685b..d865751 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,20 @@ For hybrid mode (migration scenarios), resolver patterns, error handling, and ca - **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling - **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached) +### 8. Anonymous Sessions + +[Anonymous Sessions](https://auth0.com/docs) give a visitor an Auth0 identity before they log in. The access token issued for an anonymous session is a standard Auth0 Bearer JWT, so `verify_access_token()` and `verify_request()` validate it exactly like any other token. The only difference is the `sub` claim, which starts with `anon@`. + +An anonymous token passes verification by default. If a route must not serve anonymous callers, check the `sub` claim after verification and reject it in your handler: + +```python +claims = await api_client.verify_request(headers=headers) +if claims.get("sub", "").startswith("anon@"): + raise PermissionError("Anonymous callers are not allowed on this route") +``` + +Deciding whether an anonymous caller is authorized is your application's responsibility. See [Anonymous Callers](EXAMPLES.md#anonymous-callers) for allow, block-per-route, and block-globally patterns. + ## Feedback ### Contributing