Anonymous Sessions work best when you configure appropriate session lifetimes, encrypt session tokens in production, and validate tokens server-side. These recommendations cover security, performance, and common implementation patterns to help keep your implementation fast and reliable.
Security
Auth0 recommends sessions of 30 days or longer for most applications. Sessions shorter than 24 hours are generally only useful in shared computer environments — for example, library kiosks — where multiple users access the same device.
{ "anonymous_session_lifetime": 2592000 }
Sessions shorter than 24 hours on personal devices will likely result in data loss, as users may not return within that timeframe. This leads to a poor user experience and lost cart or preference data.
If you do not need to read what the user has already provided in previous interactions, keep session tokens encrypted (JWE) in production. A potential attacker cannot see the contents of an encrypted token.
{ "anonymous_session_token_format": "jwe" }
JWE:
- Hides session metadata from users
- Prevents token inspection attacks
- Maintains session privacy
Validate tokens server-side
Always validate anonymous session tokens on your server, even if Auth0 issued them:
// In your API middleware
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://YOUR_DOMAIN/.well-known/jwks.json')
);
async function validateAnonymousToken(token) {
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://YOUR_DOMAIN/',
audience: 'YOUR_AUDIENCE',
});
// Verify it's an anonymous user
if (!payload.sub?.startsWith('anon|')) {
throw new Error('Not an anonymous session');
}
return payload;
} catch (error) {
throw new Error('Invalid token');
}
}
Limit sensitive operations
Restrict what anonymous users can do. Anonymous users have never proved their identities:
// In your API
app.post('/api/checkout', async (req, res) => {
const user = req.auth;
if (user.sub.startsWith('anon|')) {
// Anonymous users can only checkout with guest checkout
// Require email for order tracking
if (!req.body.email) {
return res.status(400).json({
error: 'Email required for guest checkout',
});
}
}
// Process checkout...
});
Never trust metadata from clients without validation. Validate structure and types before migrating anonymous data to a user profile:
// In your Action
exports.onExecutePostLogin = async (event, api) => {
if (event.anonymous_session?.metadata) {
const { cart, preferences } = event.anonymous_session.metadata;
// Validate cart structure
if (cart && Array.isArray(cart.items)) {
const sanitizedCart = cart.items
.filter(
(item) =>
typeof item.sku === 'string' &&
typeof item.qty === 'number'
)
.slice(0, 100); // Max 100 items
api.user.setAppMetadata('migrated_cart', {
items: sanitizedCart,
});
}
// Validate preferences
if (
preferences?.theme &&
['light', 'dark'].includes(preferences.theme)
) {
api.user.setUserMetadata('preferences', {
theme: preferences.theme,
});
}
}
};
Cache access tokens
Access tokens can be reused until they expire. Avoid fetching a new token on every request:
class TokenCache {
constructor() {
this.tokens = new Map();
}
async getToken(audience) {
const cached = this.tokens.get(audience);
if (cached && cached.expiresAt > Date.now()) {
return cached.token;
}
// Get new token
const response = await anonymousClient.getAccessToken({ audience });
this.tokens.set(audience, {
token: response.access_token,
expiresAt:
Date.now() + response.expires_in * 1000 - 60000, // 1 min buffer
});
return response.access_token;
}
}
Avoid frequent metadata updates. Batch changes into a single call:
// Avoid: multiple API calls
await updateMetadata({ cart_items: 1 });
await updateMetadata({ last_viewed: 'product-1' });
await updateMetadata({ preferences: { theme: 'dark' } });
// Prefer: single API call
await updateMetadata({
cart_items: 1,
last_viewed: 'product-1',
preferences: { theme: 'dark' },
});
Minimize metadata size for better performance. Store references, not full objects:
// Avoid: storing full product data
{
"cart": [
{
"id": 1,
"name": "Product1",
"description": "...",
"images": [...],
"price": 99.99
}
]
}
// Prefer: storing only references
{
"cart": [
{ "sku": "PROD-001", "qty": 2 },
{ "sku": "PROD-002", "qty": 1 }
]
}
// Or: store a cart ID and fetch items from your own system
{
"cart_id": "https://my_webstore.com/carts/cart_1234"
}
Implementation
Handle session expiration gracefully
When a session expires, create a new one rather than letting the error surface to users:
class AnonymousSessionManager {
async getSession() {
try {
return await anonymousClient.getSession();
} catch (error) {
if (error.error === 'session_expired') {
return await anonymousClient.createSession();
}
throw error;
}
}
async refreshIfNeeded() {
const session = await this.getSession();
const expiresIn = session.expires_at - Date.now();
if (expiresIn < 3600000) {
// Refresh if expiring within one hour
return await anonymousClient.updateSession({});
}
return session;
}
}
Implement offline support
Queue metadata updates when the user is offline and sync when they reconnect:
class OfflineAwareSession {
constructor() {
this.pendingUpdates = [];
}
async updateMetadata(metadata) {
try {
await anonymousClient.updateSession({ metadata });
} catch (error) {
if (!navigator.onLine) {
this.pendingUpdates.push(metadata);
this.saveToLocalStorage();
return;
}
throw error;
}
}
async syncPendingUpdates() {
if (!this.pendingUpdates.length) return;
const merged = this.pendingUpdates.reduce(
(acc, update) => ({ ...acc, ...update }),
{}
);
await anonymousClient.updateSession({ metadata: merged });
this.pendingUpdates = [];
}
}
// Sync when back online
window.addEventListener('online', () => {
sessionManager.syncPendingUpdates();
});
Clear anonymous data after login
If the anonymous session data is no longer needed after a user authenticates, clean it up:
// After login callback
async function handleLoginCallback() {
const { isAuthenticated } = await auth0.handleRedirectCallback();
if (isAuthenticated) {
// Clear anonymous session
await anonymousClient.logout();
// Or just clear local storage
localStorage.removeItem('anon_session');
// Redirect to authenticated experience
window.location.href = '/dashboard';
}
}
Test session transfer thoroughly
Test the full range of transfer scenarios:
describe('Session Transfer', () => {
it('should migrate cart on signup', async () => {
const session = await createAnonymousSession({
metadata: { cart: [{ sku: 'TEST-001', qty: 2 }] },
});
await signUp({
email: 'test@example.com',
password: 'Test123!',
anonymous_session_token: session.session_token,
});
const user = await getUser('test@example.com');
expect(user.app_metadata.migrated_cart).toEqual([
{ sku: 'TEST-001', qty: 2 },
]);
});
it('should merge carts on login', async () => {
await updateUser('user@example.com', {
app_metadata: { cart: [{ sku: 'EXISTING-001', qty: 1 }] },
});
const session = await createAnonymousSession({
metadata: { cart: [{ sku: 'ANON-001', qty: 3 }] },
});
await login({
email: 'user@example.com',
anonymous_session_token: session.session_token,
});
const user = await getUser('user@example.com');
expect(user.app_metadata.cart.length).toBe(2);
});
});
Common anti-patterns
Storing sensitive data
Never store sensitive information in anonymous session metadata:
// Never do this
{
"metadata": {
"credit_card": "4111111111111111",
"ssn": "123-45-6789"
}
}
// Store only non-sensitive references
{
"metadata": {
"cart": [{ "sku": "ITEM-001", "qty": 1 }],
"preferences": { "theme": "dark" }
}
}
Infinite session lifetimes
Avoid infinite session lifetimes in production. A device may change hands:
// Avoid: infinite lifetime
{ "anonymous_session_lifetime": 0 }
// Prefer: finite lifetime (for example, 90 days)
{ "anonymous_session_lifetime": 7776000 }
Learn more