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
Summary
When an anonymous session expires,
AnonymousSessionClient.getAccessToken()silently creates a brand-new anonymous identity and returnssessionReplaced: true. This design leaves callers with no ability to recover the expired session's data — the oldsub,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
getAccessTokenis called with an expired session token, the platform returns400 session_expiredor400 invalid_session_token. The client catches this silently and creates a fresh session:The caller receives a valid
AnonymousSession— but for a completely different identity. The oldsubis gone. Any metadata attached at session creation is gone. The only signal issessionReplaced: trueon the returned object.Why
sessionReplaced: trueDoes Not Help1. The old identity data is unrecoverable at this point
GetAnonymousAccessTokenOptionsonly accepts asessionToken: string— a raw opaque JWE that the SDK cannot decode:By the time
sessionReplaced: trueis returned, there is no way to surface what was lost — not the oldsub, not themetadata, notcreatedAt. The method signature structurally prevents it.2. Framework SDKs strip
sessionReplacedbefore it reaches app codeIn
auth0-spa-js,getTokenSilently()callsgetAccessTokeninternally but does not surfacesessionReplacedin 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:subwas (cannot clean up server-side state keyed to the old identity)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_idthat links the anonymous user to items they've added. When the session silently replaces, thatcart_idis gone. The new identity has no metadata. The app developer cannot do a guest cart merge because they don't know the oldsuborcart_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
subchange 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-jspartially mitigates this by detecting the token mismatch and throwingAnonymousSessionExpiredErrorinstead of silently replacing. But the error itself carries no context either — the server-side developer knows the session expired but still cannot access the oldsubor metadata to handle it meaningfully.Affected Layers
This spans three SDKs:
auth0-auth-js—getAccessTokensilently replaces with no recovery dataauth0-spa-js—getTokenSilently()does not surfacesessionReplacedto callers at allauth0-server-js—AnonymousSessionExpiredErroris thrown with no context, even though the server-side store has the fullsub/metadataavailable at the throw site