Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
796c9ed
Added Anonymous Session Support Changes
rmad17 Aug 11, 2026
a2a0afe
docs: clean up, formatting improvement and docs content update
rmad17 Aug 13, 2026
4c1a173
chore: renaming of classes to bring consistency, adding fallback with…
rmad17 Aug 13, 2026
118b25d
chore: trimmed comments
rmad17 Aug 14, 2026
5d6f511
docs: Optimized example docs
rmad17 Aug 14, 2026
c5d5cff
docs: Updated docs to remove semicolon
rmad17 Aug 14, 2026
29e6e0f
Merge branch 'main' into feat/anonymous-sessions
rmad17 Aug 14, 2026
cfb000a
fix: rename Anonymous Session errors to add Session in the error name…
rmad17 Aug 19, 2026
ee70a38
fix: Resolved logout error correctly
rmad17 Aug 19, 2026
3b25bcf
fix: make AnonymousClient domain typed
rmad17 Aug 19, 2026
307ba7d
fix: widened metadata types
rmad17 Aug 19, 2026
c3e6fd9
fix: session_token, sub, session_id in _remint now use explicit is no…
rmad17 Aug 19, 2026
6304af8
fix: seperate response structures for session token in create and remint
rmad17 Aug 20, 2026
edeb3d1
test: extract shared OneSlotStore fake into store_fakes
rmad17 Aug 20, 2026
bc40a56
fix: run anonymous session domain-mismatch check in static mode too
rmad17 Aug 20, 2026
15e4732
test: cover corrupted anonymous context in introspect and logout
rmad17 Aug 20, 2026
61d727e
fix: drop sub,session_id from anonymous sessions response, fix logout…
rmad17 Aug 21, 2026
ead816f
fix: remove is_new from AnonymousSession, unused signal never in spec
rmad17 Aug 23, 2026
090b798
docs: remove is_new from AnonymousSession field list
rmad17 Aug 23, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ test-script.py
coverage.xml

# AI tools
.claude
.claude
.worktrees
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ Let a logged-in user manage their own enrolled authentication methods — enroll

Bind tokens to a key your server holds ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) so a stolen token alone cannot be replayed. DPoP is supported for Passkey sign-in (`signin_with_passkey`) and the authentication-methods/factors methods on `MyAccountClient`. For key generation and usage, see [examples/Passkeys.md](examples/Passkeys.md#3-dpop-bound-passkey-tokens-optional) and [examples/MyAccountAuthenticationMethods.md](examples/MyAccountAuthenticationMethods.md#dpop).

### 10. Anonymous Sessions

Give a visitor an Auth0 `anon@<uuid>` identity before they log in, so cart/preference metadata attached pre-login is available to Post-Login Actions once they do. Requires a separate `anonymous_store` instance — never the same instance as `state_store` — and a tenant-level paid add-on flag. For setup, the token renewal ladder, login injection, and the store-isolation requirement, see [examples/AnonymousSessions.md](examples/AnonymousSessions.md).

## Feedback

### Contributing
Expand Down
129 changes: 129 additions & 0 deletions examples/AnonymousSessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Anonymous Sessions

Anonymous Sessions give a visitor an Auth0 identity **before they log in**. Each visitor gets a persistent `anon@<uuid>` subject plus an access token, with up to 1 KB of key/value metadata (cart, preferences) attached at creation. At login, the session token rides into `/authorize` so Post-Login / Pre-User-Registration Actions can read the anonymous data via `event.anonymous_session` — nothing migrates onto the real user profile automatically; the Action author decides what to persist.

> [!NOTE]
> Anonymous Sessions support for server SDKs is in Early Access, gated by a tenant-level, paid add-on feature flag (`anonymous_sessions_enabled`). `auth0-server-python` mounts no routes and sets no cookies — this guide covers the framework-agnostic core only.

## Table of Contents

- [Anonymous Sessions](#anonymous-sessions)
- [Table of Contents](#table-of-contents)
- [Setup](#setup)
- [Creating a Session](#creating-a-session)
- [Getting a Token (Renewal Ladder)](#getting-a-token-renewal-ladder)
- [Introspecting a Session](#introspecting-a-session)
- [Logging Out](#logging-out)
- [Login Injection](#login-injection)
- [Rate-Limiting `get_token()`](#rate-limiting-get_token)
- [Error Handling](#error-handling)
- [Known Limitations](#known-limitations)
- [Additional Resources](#additional-resources)

## Setup

Before using the anonymous sessions API, the `anonymous_sessions_enabled` flag must be turned on for your tenant (contact your Auth0 account team — there is no self-serve path yet), and the application/client must be enabled for the feature.

Pass an `anonymous_store` to `ServerClient`, alongside your existing `state_store` and `transaction_store`:

```python
server_client = ServerClient(
domain="your-tenant.auth0.com",
client_id="...",
client_secret="...",
secret="...",
state_store=my_state_store,
transaction_store=my_transaction_store,
anonymous_store=my_anonymous_store, # its own store instance, not state_store
)
```

Give `anonymous_store` its own store instance, not `state_store` with a different identifier. If you omit it, every `.anonymous.*` call raises `ConfigurationError` before any write.

## Creating a Session

```python
session = await server_client.anonymous.create_session(
audience="https://api.example.com",
scope="read:cart write:cart",
metadata={"cart_id": "cart_456"},
store_options=store_options,
)
```

`metadata` is **set once, at creation, and never updated** — there is no platform update endpoint for anonymous sessions. Any JSON-serializable value is accepted, ≤1 KB total (UTF-8 JSON byte length); oversized, non-JSON-serializable, or dangerous-key (`__proto__`, `constructor`, `prototype`) metadata is rejected client-side before any network call.

`AnonymousSession` returns `session_token`, `access_token`, `expires_at`, `session_expires_at`, and `metadata`.

## Getting a Token

```python
token = await server_client.anonymous.get_token(store_options=store_options)
```

Renewal logic, in order:

1. Cached access token still fresh → returned with no network call.
2. Expired → re-minted using the stored session token (not a refresh-token grant — anonymous sessions never issue refresh tokens).
3. Session token also expired or invalid → a **brand-new session is silently created, once**. Metadata from the old session is permanently lost, and `session_token` changes. This never raises — an anonymous pre-login session carries no authorization, so re-minting crosses no trust boundary.
4. Any other error → raised as a typed exception. No swallow, no auto-retry beyond the one re-mint in step 3.

## Introspecting a Session

```python
status = await server_client.anonymous.introspect(store_options=store_options)
```

`introspect()` is read-only: it returns the current session status without renewing the token or changing `sub`.

## Logging Out

```python
await server_client.anonymous.logout(store_options=store_options)
```

> [!CAUTION]
> **`logout()` does not revoke.** There is no server-side anonymous session store to revoke against, this clears only the locally-held encrypted context. Any access token already issued for this anonymous session remains valid until its natural expiry.

Local state is always cleared, even if the remote call fails. If the remote `/anonymous/logout` call itself fails, `logout()` raises `AnonymousSessionLogoutError` after clearing local state, so the failure isn't swallowed.

## Login Injection

When an anonymous session is active, `start_interactive_login()` automatically includes the session token in the `/authorize` request, no code change needed at your call site. If no anonymous session exists, behavior is same as today.

The token travels as a query parameter to `/authorize`, which means it lands in browser history, `Referer` headers, and access logs. This is because the token grants no authorization on its own and the request is a browser-to-Auth0 HTTPS redirect, but you should still set `Referrer-Policy: no-referrer` on your login pages, and never log the authorize URL.

Pushed Authorization Requests (PAR) are not supported for anonymous sessions — injection is suppressed entirely on that code path.

## Rate-Limiting `get_token()`

`get_token()`'s retry-once bound caps amplification to two upstream Auth0 calls *per invocation*. It does not protect against an attacker calling your route repeatedly. `POST /anonymous/token` is an unauthenticated, token-issuing endpoint. **You must rate-limit any route in your application that calls `get_token()` on an anonymous session**, the same way you would rate-limit any other unauthenticated token-issuing path. The SDK has no request-level context to do this itself.

## Error Handling

All anonymous session errors subclass `AnonymousSessionApiError`, carrying a `.code` you can branch on:

```python
from auth0_server_python.error import (
AnonymousSessionFeatureNotEnabledError,
AnonymousSessionClientNotEnabledError,
AnonymousSessionClientNotSupportedError,
AnonymousSessionResourceServerError,
AnonymousSessionScopeError,
AnonymousSessionCreateError,
AnonymousSessionTokenError,
AnonymousSessionIntrospectError,
AnonymousSessionLogoutError,
)

try:
session = await server_client.anonymous.create_session(audience="...", scope="...")
except AnonymousSessionFeatureNotEnabledError:
...
```

## Known Limitations

- **DPoP is not supported.** `AnonymousClient` has no `dpop_key` parameter anywhere in its public API. A tenant/client configured with `require_proof_of_possession: true` cannot use anonymous sessions, you will see `AnonymousSessionClientNotSupportedError`.
- **PAR, CIBA, Device Flow, RAR, and mTLS clients are not supported** for anonymous sessions.
- **No server-side revocation.** See [Logging Out](#logging-out) above.
3 changes: 2 additions & 1 deletion src/auth0_server_python/auth_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .anonymous_client import AnonymousClient
from .mfa_client import MfaClient
from .my_account_client import MyAccountClient
from .server_client import ServerClient

__all__ = ["ServerClient", "MyAccountClient", "MfaClient"]
__all__ = ["ServerClient", "MyAccountClient", "MfaClient", "AnonymousClient"]
Loading