Skip to main content
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

Configure appropriate session lifetimes

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.

Use JWE token format

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...
});

Sanitize metadata

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,
      });
    }
  }
};

Performance

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;
  }
}

Batch metadata updates

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' },
});

Keep metadata small

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