MyZubster Metaverse Security Update: Revocable Sessions, Token Rotation, Automatic Renewal and CSRF Protection
We have completed another major security milestone for the MyZubster Metaverse.
The original authentication system primarily depended on browser-stored JWTs. That approach was simple, but it did not provide the session lifecycle controls required by a platform involving persistent identities, virtual characters, private rooms, realtime interactions and future Marketplace or payment capabilities.
Under MYZ-71 — Authentication, Sessions & Account Security, we have now implemented:
- persistent server-side sessions;
- session-bound JWT access tokens;
- real logout and remote revocation;
- active-device management;
- rotating opaque refresh tokens;
- refresh-token replay detection;
- automatic access-token renewal for the Metaverse;
- concurrent refresh deduplication;
- origin-based CSRF protection;
- structured authentication errors and request IDs.
The work is available in a Draft pull request:
https://github.com/danieldirimini-myzubster/myzubster/pull/10
The pull request has not been merged and the system has not yet been declared production-ready.
Why we changed the authentication architecture
A completely stateless JWT architecture creates several security and operational limitations.
A cryptographically valid token can remain usable even when:
- the user logs out;
- the device should no longer be trusted;
- the password has changed;
- the account owner removes another device;
- an administrator needs to terminate a compromised session.
Long-lived access tokens increase the impact of credential theft. Storing them in localStorage also makes them accessible to JavaScript, increasing the consequences of an XSS vulnerability.
We therefore moved to a hybrid architecture:
text
User authentication
↓
Persistent server-side session
↓
Short-lived JWT access token
↓
Rotating opaque refresh token
↓
Automatic renewal
↓
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
Every authenticated device now receives a separate MongoDB AuthSession.
A session stores:
- a unique session ID;
- the authenticated user ID;
- creation time;
- last activity;
- expiration time;
- device information;
- privacy-preserving network metadata;
- revocation time and reason;
- the current refresh-token hash;
- a bounded history of consumed refresh-token hashes;
- refresh-token rotation metadata.
MongoDB TTL indexing is used to clean up expired sessions automatically.
This allows the backend to reject an otherwise valid JWT when its session has been:
- revoked;
- deleted;
- expired;
- associated with another user;
- invalidated by a security event.
Session-bound access tokens
New JWT access tokens contain a sid claim identifying the persistent session.
A simplified token payload looks like this:
{
"sub": "user-id",
"sid": "session-uuid",
"jti": "access-token-uuid",
"iat": 1790540000,
"exp": 1790540900
}
Authentication now requires two validations:
Verify JWT signature and expiration
↓
Load and validate the referenced server session
This gives us both efficient request authentication and immediate server-side revocation.
Real logout and device management
The new account-security page is available at:
/account/security
Users can inspect:
- active sessions;
- the current device;
- creation time;
- last activity;
- expiration time;
- limited device information.
They can revoke another device or terminate their current session.
The relevant endpoints are:
POST /api/auth/logout
GET /api/auth/me
GET /api/auth/me/sessions
DELETE /api/auth/me/sessions/:sessionId
Logout is idempotent.
Even if the access token is malformed, expired or already revoked, the backend still clears the authentication cookies. When possible, it also identifies and revokes the session through the refresh token.
Rotating opaque refresh tokens
Refresh tokens are opaque rather than JWT-based.
Their conceptual structure is:
myzr.<session-id>.<cryptographically-random-secret>
The raw refresh token is never stored in MongoDB.
The server calculates an HMAC-SHA256 digest:
stored digest = HMAC-SHA256(server secret, raw refresh token)
Only this digest is persisted.
If the database is exposed, the stored digest cannot be used directly as a refresh token.
Atomic refresh-token rotation
The refresh endpoint is:
POST /api/auth/refresh
Every refresh token is single-use.
When the browser calls the endpoint, the server:
1. reads the HttpOnly refresh cookie;
2. extracts the session identifier;
3. hashes the submitted token;
4. creates a new random refresh token;
5. atomically verifies the current stored hash;
6. replaces it with the new hash;
7. records the consumed hash;
8. issues a new access token;
9. returns new authentication cookies.
The atomic comparison prevents two requests from successfully consuming the same refresh token.
Replay detection
Consumed refresh-token hashes are retained in a bounded history.
If an old token is submitted again, the server treats it as a possible credential compromise:
Previously consumed token submitted
↓
Refresh-token replay detected
↓
Entire server-side session revoked
↓
Cookies cleared
↓
New authentication required
The revocation reason is recorded as:
refresh-token-replay
The consumed-token history is limited to prevent unbounded MongoDB document growth.
Automatic access-token renewal
Rotating refresh tokens are only useful if the frontend can renew access tokens safely.
We introduced a reusable client:
authenticatedFetch(input, options)
The client:
1. sends the protected request;
2. detects a recognized access-token 401;
3. calls /api/auth/refresh;
4. stores the temporary migration access token;
5. retries the original request exactly once;
6. returns the retried response.
The complete Metaverse API client now uses this authenticated request layer.
This includes operations related to:
- Metaverse profiles;
- rooms;
- invitations;
- room sessions;
- participants;
- stage access;
- messages;
- reports;
- moderation;
- landmarks;
- realtime world actions.
Concurrent refresh deduplication
A browser may issue multiple protected requests at the same time.
If the access token expires, several requests could receive a 401 simultaneously. Without coordination, every request would try to consume the same single-use refresh token.
That could incorrectly trigger replay protection.
The frontend therefore shares one in-flight refresh promise:
Request A → 401 ┐
Request B → 401 ├─→ One refresh operation
Request C → 401 ┘
↓
New access token
↓
Retry A, B and C once
Our concurrency test verifies that two simultaneous authentication failures produce:
- exactly one refresh request;
- two successful retries;
- no accidental refresh-token replay.
Terminal authentication failures
The frontend does not try to silently refresh an explicitly revoked session.
For example:
AUTH_SESSION_REVOKED
is returned directly to the application.
If token renewal fails permanently, the client:
- clears the temporary browser credentials;
- emits the event:
myzubster:auth-expired
This gives the UI a centralized signal for returning the user to authentication while preserving structured error information.
CSRF protection
Moving authentication into cookies changes the threat model.
Cookies are attached automatically by the browser, so state-changing requests require Cross-Site Request Forgery protection.
We added an origin-based CSRF guard for unsafe cookie-authenticated requests.
Protected methods include:
POST
PUT
PATCH
DELETE
The guard checks:
- the Origin header;
- the Referer header when necessary;
- the Sec-Fetch-Site browser metadata;
- the configured list of trusted application origins;
- the request’s actual target origin.
Requests marked as:
Sec-Fetch-Site: cross-site
are rejected before the backend queries the session database.
The structured response is:
{
"success": false,
"request_id": "request-uuid",
"error": {
"code": "AUTH_CSRF_REJECTED",
"message": "Origine della richiesta non autorizzata"
}
}
Protected authentication mutations
The trusted-origin middleware now protects:
Password registration
Password login
OAuth ticket verification
Social-login ticket exchange
Refresh-token rotation
Logout
Gmail verification exchange
Cookie-authenticated protected mutations
Bearer-token clients remain compatible during the migration period because they do not rely on automatically attached browser cookies.
Non-browser CLI and server-to-server clients that send neither browser origins nor Fetch Metadata also remain compatible.
Configuring trusted origins
Production and preview environments can declare trusted frontend origins with:
AUTH_TRUSTED_ORIGINS=https://www.myzubster.com,https://preview.myzubster.com
The middleware also recognizes existing configuration values:
FRONTEND_URL
PUBLIC_APP_URL
GATEWAY_PUBLIC_URL
In development, the standard local React origins are accepted:
http://localhost:3000
http://127.0.0.1:3000
Each production and preview domain still needs to be verified before cookie-only authentication is enabled.
Cookie configuration
Access and refresh cookies use:
HttpOnly
SameSite=Lax
Secure in production
The refresh cookie is additionally limited to:
Path=/api/auth
This reduces the number of requests carrying the refresh credential.
SameSite=Lax provides an additional browser-level defence, but it is not treated as the only CSRF control.
Privacy-preserving session metadata
The backend does not store raw client IP addresses.
Instead, it stores an HMAC-derived digest.
This provides a limited security correlation signal without retaining the original address as normal application data.
Device metadata is also limited to what is useful for recognizing active sessions.
Authentication security should not become an unnecessary tracking mechanism.
Validation results
The focused backend security scope currently passes:
30/30 tests
The focused frontend MYZ-71 and Metaverse scope passes:
19/19 tests
The test coverage includes:
- persistent session creation;
- session-bound JWTs;
- logout;
- device revocation;
- refresh-token hashing;
- atomic token rotation;
- replay detection;
- replay-driven session revocation;
- password and social authentication;
- automatic access-token renewal;
- exactly-once retry;
- concurrent refresh deduplication;
- structured Metaverse errors;
- same-origin CSRF acceptance;
- configured frontend-origin acceptance;
- cross-site rejection;
- rejection before session lookup;
- bearer-token compatibility;
- non-browser client compatibility.
The optimized React production build also completes successfully.
GitHub Actions results
The affected security suites are green in GitHub Actions:
tests/authSessionService.test.js
tests/authSessionController.test.js
tests/authMiddlewareSessions.test.js
tests/socialIdentityService.test.js
tests/csrfProtection.test.js
The complete backend result currently reports:
142 test suites passed
42 test suites failed
746 tests passed
44 tests failed
The remaining failures are existing repository-wide baseline problems involving:
- Zorgax;
- Marketplace flows;
- legacy Metaverse expectations;
- seller policies;
- older source-code assertion tests.
The Security Audit and MYZ-164 workflows also remain red because of previously recorded baseline conditions.
The new MYZ-71 implementation is green in its affected suites, but the pull request remains Draft because the repository as a whole is not yet CI-clean.
Current implementation status
Persistent server-side sessions DONE
Session-bound access tokens DONE
Real logout DONE
Active-device listing DONE
Remote session revocation DONE
Opaque refresh tokens DONE
Atomic refresh rotation DONE
Refresh-token replay detection DONE
Password and social-session integration DONE
Account-security interface DONE
Metaverse automatic token renewal DONE
Concurrent refresh deduplication DONE
Origin-based CSRF protection DONE
Structured authentication errors DONE
Focused backend and frontend validation DONE
Production trusted-origin validation PENDING
Renewal for non-Metaverse API clients PENDING
Removal of localStorage authentication PENDING
Removal of bearer migration support PENDING
Strict server-session enforcement PENDING
Passkeys and magic links PENDING
Step-up authentication PENDING
Production OAuth verification PENDING
MongoDB Atlas production validation PENDING
Repository-wide green CI PENDING
What remains
Extend renewal beyond the Metaverse
The reusable authenticated client currently protects the complete Metaverse API layer.
Other authenticated areas still contain direct fetch calls and manually constructed bearer headers.
These modules must be migrated gradually to the same centralized client.
Remove access tokens from localStorage
localStorage remains temporarily supported as a migration bridge.
The final architecture should not expose reusable authentication credentials to frontend JavaScript.
Removing it requires migrating:
- Marketplace clients;
- Zorgax clients;
- account and profile APIs;
- protected administrative views;
- realtime connections;
- any embedded or mobile clients.
Validate the public deployment
Before enabling cookie-only authentication, we must verify:
- the exact production origin;
- every preview origin;
- access-cookie expiration;
- refresh-cookie path restrictions;
- proxy protocol and host forwarding;
- CSRF rejection on the public API;
- successful same-origin renewal;
- remote session revocation;
- deliberate refresh-token replay.
Enable strict server sessions
After legacy tokens have expired, the platform can enable:
REQUIRE_SERVER_SESSION=true
At that point, JWTs without a valid persistent session will no longer be accepted.
Add passkeys and magic links
The roadmap still includes passwordless authentication through flows such as:
/auth/start
/auth/verify
Passkeys require correct WebAuthn handling for challenges, relying-party configuration, credential counters, revocation and recovery.
Magic links require short-lived, single-use verification tokens with replay protection.
Add step-up authentication
Sensitive operations should require recent stronger verification.
Examples include:
- changing the primary email;
- changing a password;
- adding or deleting a passkey;
- deleting an account;
- accessing wallet functionality;
- approving payment operations;
- changing sensitive Marketplace settings.
Validate OAuth and MongoDB Atlas
Google, GitHub and Facebook authentication must still be tested using real production configuration.
MongoDB Atlas must also be checked for:
- TTL-index creation;
- refresh-token indexes;
- document growth;
- expired-session cleanup;
- concurrent updates;
- rolling-deployment compatibility.
Recommended rollout sequence
1. Review the Draft pull request
2. Configure production trusted origins
3. Deploy with bearer compatibility enabled
4. Verify cookies and MongoDB indexes
5. Test automatic Metaverse renewal
6. Test concurrent renewal
7. Test cross-site request rejection
8. Test deliberate refresh-token replay
9. Extend authenticatedFetch to other modules
10. Allow legacy tokens to expire
11. Remove localStorage authentication
12. Enable strict server sessions
13. Remove bearer migration support
14. Add passkeys, magic links and step-up authentication
Conclusion
The MyZubster Metaverse now has a substantially stronger authentication foundation.
We moved from browser-managed JWTs toward a complete session lifecycle:
Authenticate
Create session
Issue access
Rotate refresh
Renew automatically
Detect replay
Revoke remotely
Validate origin
Recover securely
The most important backend and Metaverse frontend controls are now implemented and covered by focused tests.
The next stage is deployment validation and migration: trusted production origins, renewal across the rest of the frontend and removal of browser-accessible credentials.
The current implementation is available in the Draft pull request:
https://github.com/danieldirimini-myzubster/myzubster/pull/10
Top comments (0)