Skip to content

getAccessToken silently replaces expired sessions — sessionReplaced: true provides no recovery path #287

Description

@cschetan77

Summary

When an anonymous session expires, AnonymousSessionClient.getAccessToken() silently creates a brand-new anonymous identity and returns sessionReplaced: true. This design leaves callers with no ability to recover the expired session's data — the old sub, metadata, and creation timestamp are permanently unrecoverable at the call site. For any application that does meaningful work with anonymous identity state, this silent loss cannot be handled.


What Happens Today

When getAccessToken is called with an expired session token, the platform returns 400 session_expired or 400 invalid_session_token. The client catches this silently and creates a fresh session:

// anonymous-session-client.ts
try {
  return await this.#mintToken(options.sessionToken, options);
} catch (e) {
  if (e instanceof AnonymousSessionError && SESSION_INVALIDATION_CODES.has(e.code)) {
    const fresh = await this.createSession({ audience: options?.audience, scope: options?.scope });
    return { ...fresh, sessionReplaced: true };
  }
  throw e;
}

The caller receives a valid AnonymousSession — but for a completely different identity. The old sub is gone. Any metadata attached at session creation is gone. The only signal is sessionReplaced: true on the returned object.


Why sessionReplaced: true Does Not Help

1. The old identity data is unrecoverable at this point

GetAnonymousAccessTokenOptions only accepts a sessionToken: string — a raw opaque JWE that the SDK cannot decode:

interface GetAnonymousAccessTokenOptions {
  sessionToken?: string;  // opaque JWE — no sub, no metadata accessible
  audience?: string;
  scope?: string;
}

By the time sessionReplaced: true is returned, there is no way to surface what was lost — not the old sub, not the metadata, not createdAt. The method signature structurally prevents it.

2. Framework SDKs strip sessionReplaced before it reaches app code

In auth0-spa-js, getTokenSilently() calls getAccessToken internally but does not surface sessionReplaced in its own return type. The SPA developer receives a valid access token with no indication the underlying identity changed. The replacement is completely invisible.

3. The signal arrives too late for meaningful action

Even if a caller did check sessionReplaced: true, they cannot act on it — they no longer know:

  • What the old sub was (cannot clean up server-side state keyed to the old identity)
  • What metadata was on it (cannot re-attach it to the new session)
  • How old the session was (cannot distinguish "30-day expiry" from "token corrupted")

End User Impact

Anonymous sessions are positioned for e-commerce, pre-login personalisation, and guest checkout — use cases where the anonymous identity carries real application state. Silent replacement breaks these in concrete ways:

1. Guest cart loss
The most common pattern: metadata carries a cart_id that links the anonymous user to items they've added. When the session silently replaces, that cart_id is gone. The new identity has no metadata. The app developer cannot do a guest cart merge because they don't know the old sub or cart_id. From the customer's perspective: they return after 30 days to find their cart empty, with no explanation.

2. Post-Login Action correlation failure
Many apps map anonymous session metadata to authenticated user app metadata in a Post-Login Action (event.anonymous_session.metadata). After silent replacement, the new session has no metadata — the correlation produces a blank result. The mapping logic fails silently and the developer has no way to distinguish "user had no metadata" from "session was silently replaced".

3. Analytics and attribution breakage
If anonymous users are tracked across sessions for funnel analysis or attribution (e.g. "user saw homepage as anonymous, converted 29 days later"), the silent sub change severs that chain. The conversion appears to come from a brand-new user with no history. The error is invisible in logs.

4. Progressive profiling loss
Any metadata accumulated over the session's lifetime (browsing preferences, A/B cohort assignments, feature flag overrides) is permanently lost. The new identity starts blank. If the app was personalising the experience based on this data, the experience silently degrades.

5. Silent data inconsistency in server-side apps
auth0-server-js partially mitigates this by detecting the token mismatch and throwing AnonymousSessionExpiredError instead of silently replacing. But the error itself carries no context either — the server-side developer knows the session expired but still cannot access the old sub or metadata to handle it meaningfully.


Affected Layers

This spans three SDKs:

  • auth0-auth-js — getAccessToken silently replaces with no recovery data
  • auth0-spa-js — getTokenSilently() does not surface sessionReplaced to callers at all
  • auth0-server-js — AnonymousSessionExpiredError is thrown with no context, even though the server-side store has the full sub/metadata available at the throw site

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions