Ecco la versione in inglese pronta per CoderLegion.
How We Built Revocable Sessions and Refresh-Token Replay Protection for the MyZubster Metaverse
Authentication in a Metaverse platform is more than a login form.
An authenticated identity may control characters, social relationships, private spaces, Marketplace activity and, eventually, payment-related operations. For MyZubster, relying only on long-lived JSON Web Tokens was no longer a sufficient security model.
Under MYZ-71 — Authentication, Sessions & Account Security, we started replacing stateless browser authentication with persistent, revocable and observable server-side sessions.
This article describes what we implemented, how refresh-token rotation works, how we detect token replay, what we tested and what still needs to be completed before production deployment.
The implementation is currently in a Draft pull request. It has not been merged or declared production-ready.
Why stateless JWT authentication was not enough
JWTs are useful because a server can validate them without loading a database record for every request.
However, a completely stateless model creates important limitations:
- logout cannot reliably invalidate an existing token;
- users cannot see which devices are authenticated;
- administrators cannot revoke one compromised device;
- a stolen token remains valid until expiration;
- password changes do not automatically terminate sessions;
- long-lived tokens increase the potential impact of credential theft;
- tokens stored in localStorage can be accessed by malicious JavaScript after an XSS compromise.
We therefore introduced a hybrid architecture:
Authentication↓
Persistent server-side session
↓
Short-lived JWT access token
↓
Rotating opaque refresh token
↓
Replay detection and session revocation
JWTs are still used for request authentication, but the server-side session is now the final source of authority.
Persistent server-side sessions
We added a MongoDB AuthSession model.
A separate session is created for each authenticated device. The record contains:
- a unique session identifier;
- the user who owns the session;
- creation and expiration timestamps;
- the last activity timestamp;
- revocation state and reason;
- limited device metadata;
- a privacy-preserving client-IP digest;
- the current refresh-token hash;
- a bounded history of consumed refresh-token hashes;
- refresh-token rotation metadata.
MongoDB TTL indexing is used to remove expired session records automatically.
This enables real session lifecycle management. A valid JWT is rejected whenever its associated session is expired, missing or revoked.
Session-bound access tokens
New JWT access tokens include a server-generated sid claim.
A simplified payload looks like this:
{
"sub": "user-id",
"sid": "session-uuid",
"jti": "access-token-uuid",
"iat": 1790540000,
"exp": 1790540900
}
Authentication now involves two checks:
- Verify the JWT signature and expiration
- Validate the persistent session referenced by sid
This architecture preserves the advantages of JWTs while restoring the ability to revoke access immediately.
If the user removes a device from the account-security page, the session becomes invalid even if its most recent JWT has not expired.
Opaque refresh tokens
The refresh token is not a JWT and does not contain trusted user data.
Its conceptual format is:
myzr.<session-id>.<cryptographically-random-secret>
The random section is produced using a cryptographically secure random-number generator.
The complete refresh token is sent to the authenticated browser, but it is never stored directly in MongoDB.
Instead, the backend calculates:
HMAC-SHA256(server secret, raw refresh token)
Only the resulting digest is saved.
This prevents a database leak from immediately exposing usable refresh credentials.
Protected authentication cookies
Refresh tokens are stored in cookies configured with:
HttpOnly
SameSite=Lax
Secure in production
Path=/api/auth
HttpOnly prevents frontend JavaScript from reading the credential.
The restricted path reduces the number of requests carrying the refresh cookie.
Access tokens can also be received through an HttpOnly cookie. Bearer-token authentication remains temporarily supported to avoid breaking clients during the migration.
Atomic refresh-token rotation
A refresh token is single-use.
When the browser calls:
POST /api/auth/refresh
the backend:
- reads the refresh cookie;
- extracts the session identifier;
- calculates the HMAC digest of the submitted token;
- generates a new opaque token;
- atomically compares the submitted hash with the current stored hash;
- replaces the stored hash;
- records the consumed hash;
- issues a new access token;
- sends new access and refresh cookies.
The database update only succeeds if:
- the session exists;
- the session has not expired;
- the session has not been revoked;
- the submitted hash is still the current refresh-token hash.
This is effectively a compare-and-swap operation.
If two requests try to use the same refresh token simultaneously, only one can rotate it successfully.
Refresh-token replay detection
Rotation prevents normal reuse, but the system must also detect when an old token appears again.
After every successful rotation, the consumed token hash is placed in a bounded history.
If a token from that history is submitted again, the server treats it as a possible credential compromise.
Previously consumed token submitted ↓
Replay detected
↓
Entire server-side session revoked
↓
Authentication cookies cleared
↓
A new login is required
The session receives the revocation reason:
refresh-token-replay
The history is limited to the latest 50 hashes so the MongoDB document cannot grow indefinitely.
Replay does not necessarily prove malicious activity. It could also indicate an outdated client or an unusual concurrency condition. Revoking the session is nevertheless the safest response.
New authentication endpoints
The current implementation provides the following endpoints.
Refresh the session
POST /api/auth/refresh
This endpoint rotates the refresh token and issues a new access token.
The raw refresh token is never included in the JSON response.
Log out
POST /api/auth/logout
Logout is idempotent.
Authentication cookies are cleared even when the access token is missing, malformed, expired or already revoked.
When possible, the refresh token is used to find and revoke the persistent session.
Retrieve the authenticated account
GET /api/auth/me
Returns the authenticated user and safe session metadata.
List active devices
GET /api/auth/me/sessions
Returns the user’s active sessions and indicates the current device.
Revoke a device
DELETE /api/auth/me/sessions/:sessionId
Allows users to terminate another session remotely.
Revoking the current session also clears local authentication state and returns the user to the login flow.
Password and social authentication
Password registration and login now create persistent sessions and issue both access and refresh cookies.
Verified social-login flows use the same architecture.
The raw refresh token is not placed inside:
- redirect URLs;
- OAuth query parameters;
- signed callback tickets;
- public API responses.
This reduces the risk of credentials leaking through browser history, analytics systems, proxy logs or referrer headers.
Account-security interface
The React frontend now includes:
/account/security
The page displays:
- active sessions;
- the current device;
- session creation time;
- last activity;
- expiration time;
- limited device information.
Users can remotely revoke another device or terminate the current session.
The page is linked from the authenticated account panel and Neon Plaza.
The deployment configuration also supports the route as a single-page application path and applies:
X-Robots-Tag: noindex, nofollow
Account-security pages should not be indexed by search engines.
Privacy-preserving session metadata
The system does not store raw client IP addresses.
Instead, it saves an HMAC-derived digest. This provides a limited correlation signal for security purposes without retaining the original IP address as normal application data.
Device metadata is also intentionally limited.
Session security should help users recognize their devices without becoming an invasive tracking mechanism.
Legacy migration support
The existing frontend still contains API clients that expect a token in localStorage or use:
Authorization: Bearer
Removing this immediately would break existing routes and clients.
The migration is therefore divided into stages:
Persistent sessions and cookie support ↓
Rotating refresh tokens
↓
Centralized access-token renewal
↓
CSRF protection
↓
Cookie-only authentication
↓
Expiration of legacy browser tokens
↓
Strict server-session enforcement
↓
Removal of localStorage and bearer migration
An environment option is available for strict enforcement:
REQUIRE_SERVER_SESSION=true
It should only be activated after existing clients and legacy credentials have been migrated.
Testing the implementation
The focused authentication implementation currently passes:
Backend: 30/30 tests
Frontend: 12/12 tests
The React production build also completes successfully.
Real MongoDB integration coverage verifies the complete replay lifecycle:
Create persistent session
↓
Use current refresh token
↓
Rotate successfully
↓
Submit the old token again
↓
Reject the replay
↓
Revoke the server-side session
↓
Persist refresh-token-replay as the reason
The following authentication suites also pass in GitHub Actions:
tests/authSessionService.test.js
tests/authSessionController.test.js
tests/socialIdentityService.test.js
Repository-wide CI status
The complete repository pipeline is not yet green.
The latest full backend execution reported:
141 test suites passed
42 test suites failed
738 tests passed
44 tests failed
The failing tests mainly concern pre-existing behavior in:
- Zorgax payments and payment intents;
- Marketplace flows;
- seller-membership policies;
- older Metaverse authentication expectations;
- Metaverse observability;
- legacy source-code assertion tests.
The Security Audit and MYZ-164 policy workflows also remain red because of previously identified repository-wide conditions.
The new MYZ-71 session and refresh-token suites are green, but the pull request must remain a Draft until the wider baseline and the remaining authentication work are addressed.
What still needs to be implemented
Centralized access-token renewal
The frontend needs a shared API layer that can:
- identify an expired access token;
- call /api/auth/refresh;
- retry the original request;
- prevent simultaneous refresh calls;
- redirect to login when renewal fails.
Without a centralized implementation, individual pages may respond differently when the access token expires.
CSRF protection
Cookie-based authentication requires explicit protection for state-changing operations.
The final design should combine appropriate controls such as:
- origin validation;
- CSRF tokens;
- double-submit cookies;
- carefully selected SameSite policies;
- rejection of unsafe cross-origin requests.
SameSite=Lax should not be considered the only defence.
Removal of browser-stored tokens
The current frontend still updates the legacy localStorage token as a temporary compatibility bridge.
The final architecture should not expose reusable authentication credentials to frontend JavaScript.
Before removing localStorage, we must migrate:
- every authenticated API client;
- protected React routes;
- realtime and websocket authentication;
- mobile or embedded clients;
- old sessions and legacy JWTs.
Passkeys and magic links
The MYZ-71 roadmap includes passwordless authentication based on flows such as:
/auth/start
/auth/verify
Passkey support will require correct WebAuthn management for:
- relying-party configuration;
- allowed origins;
- challenge creation and expiration;
- credential counters;
- credential revocation;
- account recovery.
Magic links will require short-lived, single-use verification tokens and replay protection.
Step-up authentication
Sensitive operations should require recent stronger verification.
Examples include:
- changing the primary email;
- changing a password;
- registering or deleting passkeys;
- deleting the account;
- accessing wallet or payment functionality;
- modifying high-impact Marketplace settings.
A valid session should not automatically authorize every sensitive action indefinitely.
Password-change session policy
The application must define what happens after a password change:
- revoke every session;
- keep only the current session;
- allow the user to choose;
- require step-up authentication.
This policy must be enforced on the server and tested with real session records.
Production OAuth validation
Google, GitHub and Facebook login still need production verification for:
- exact callback URLs;
- public frontend and API origins;
- provider secrets;
- redirect allowlists;
- account-linking behavior;
- missing-email cases;
- cookie behavior across domains.
MongoDB Atlas validation
The production deployment must verify:
- TTL-index creation;
- refresh-token indexes;
- cleanup of expired sessions;
- document growth;
- rolling-deployment compatibility;
- session behavior under concurrent refresh requests.
Rate limiting
Authentication endpoints need dedicated abuse protection.
This includes limits for:
- password login;
- refresh requests;
- password recovery;
- magic-link generation;
- WebAuthn challenge creation;
- social-auth callbacks.
Security monitoring
Structured security events should be produced for:
- login success and failure;
- session creation;
- session revocation;
- remote device removal;
- refresh-token replay;
- password changes;
- passkey registration;
- suspicious authentication activity.
Logs must never contain passwords, raw access tokens, raw refresh tokens, OAuth authorization codes or provider secrets.
Recommended deployment sequence
The safest rollout order is:
- review the Draft pull request;
- deploy with legacy compatibility enabled;
- verify MongoDB indexes;
- validate cookies using the real public domains;
- test login, refresh, logout and remote revocation;
- execute a controlled refresh-token replay test;
- implement centralized frontend renewal;
- add and validate CSRF protection;
- monitor session and replay events;
- allow legacy credentials to expire;
- activate strict server-session enforcement;
- remove localStorage and legacy bearer authentication;
- introduce passkeys, magic links and step-up authentication.
Current progress
Persistent server-side sessions DONE
Session-bound JWT access tokens DONE
Real logout DONE
Active-device listing DONE
Remote session revocation DONE
Opaque rotating refresh tokens DONE
Refresh-token replay detection DONE
Password and social-login integration DONE
Account-security interface DONE
Focused backend and frontend testing DONE
Centralized access-token renewal PENDING
CSRF hardening PENDING
Cookie-only migration PENDING
Removal of localStorage authentication PENDING
Passkeys and magic links PENDING
Step-up authentication PENDING
Production OAuth validation PENDING
MongoDB Atlas production validation PENDING
Repository-wide green CI PENDING
Final thoughts
Secure authentication is not just about generating a token.
It requires a complete lifecycle:
Issue
Verify
Renew
Observe
Revoke
Recover
Monitor
MyZubster now has the foundation for persistent multi-device sessions, secure refresh-token rotation and automatic replay response.
The next major objective is completing the browser migration safely through centralized renewal, CSRF protection and the removal of JavaScript-accessible authentication credentials.
Only after those controls are validated should strict session enforcement be activated in production.
The current implementation can be reviewed in the Draft pull request:
https://github.com/danieldirimini-myzubster/myzubster/pull/10