Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,74 @@ 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 is verified like any other. It must be issued for this API's audience. 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
from auth0_api_python import ApiClient, ApiClientOptions

async def handle_cart(headers):
api_client = ApiClient(ApiClientOptions(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing worth flagging for readers. Building the ApiClient inside the handler means the discovery and JWKS caches are recreated on every request, so each call refetches them. This matches the other examples in the file so it is not a blocker, but a one line note to build the client once and reuse it (or pass a shared cache_adapter) would help people who copy this into a hot path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note at the end of the section: in production, build ApiClient once at startup and reuse it, or pass a shared cache_adapter, so JWKS and discovery caches persist across requests.

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"])}
```

> [!NOTE]
> These snippets construct `ApiClient` inside the handler for clarity. In production, build it once at startup and reuse it, or pass a shared `cache_adapter`, so JWKS and discovery caches persist across requests.

### Block anonymous callers on a specific route

```python
from auth0_api_python import ApiClient, ApiClientOptions

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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an API this case is usually a 403. PermissionError is fine as pseudo code, but can we add a short note to map it to a 403 in the reader's framework? That makes the intent clearer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note: blocking an anonymous caller should return an HTTP 403 Forbidden — replace PermissionError with your framework's error type.


return {"order": create_order(claims["sub"])}
```

> [!NOTE]
> Blocking an anonymous caller should return an HTTP `403 Forbidden`. Replace `PermissionError` with your framework's error type.

### 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
from auth0_api_python import ApiClient, ApiClientOptions

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.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ 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 Callers

[Anonymous Sessions](https://auth0.com/docs/manage-users/sessions/anonymous-sessions) give a visitor an Auth0 identity before they log in. Session creation happens in your web application. This SDK only validates the tokens they produce. The access token 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. 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
Expand Down
Loading