DEV Community

Daniel Ioni
Daniel Ioni

Posted on

Building Secure, Revocable Authentication Sessions for the MyZubster Metaverse

Building Secure, Revocable Authentication Sessions for the MyZubster Metaverse

Authentication is becoming a critical part of the MyZubster Metaverse.

As the platform grows, a simple JSON Web Token stored in the browser is no longer enough. We need to support multiple devices, real logout, compromised-session revocation, secure token renewal, social authentication, and eventually passkeys and step-up verification.

This article explains the authentication and session-security work completed under MYZ-71, the architectural decisions behind it, how the implementation was tested, and what still needs to be completed before the system can be considered production-ready.

The implementation is currently available in a draft pull request. It has not been merged or declared production-ready yet.

The original problem

The previous authentication model relied primarily on JWT access tokens.

JWTs are convenient because the server can verify them without querying a session database. However, completely stateless JWT authentication introduces several operational problems:

  • logging out does not necessarily invalidate an existing token;
  • a stolen token remains valid until it expires;
  • users cannot inspect their active devices;
  • administrators cannot selectively revoke a compromised session;
  • password changes cannot reliably terminate existing sessions;
  • long-lived tokens increase the impact of token theft;
  • browser storage exposes tokens to JavaScript and therefore increases the consequences of an XSS vulnerability.

For a Metaverse platform where identity may control characters, private rooms, social interactions, marketplace activity, and future payment features, those limitations are not acceptable.

We therefore moved from purely stateless authentication toward a hybrid architecture:


text
User authentication
        ↓
Persistent server-side session
        ↓
Short-lived JWT access token
        ↓
Rotating opaque refresh token
        ↓
Automatic replay detection and revocation
The JWT remains useful for efficient request authentication, but it is no longer the sole source of authority.
Persistent server-side sessions
We introduced a MongoDB AuthSession model.
Every authenticated device now receives its own persistent session. The session includes information such as:
- a unique session ID;
- the authenticated user;
- creation time;
- last activity time;
- expiration time;
- revocation time and reason;
- limited device metadata;
- a privacy-preserving client-IP digest;
- the current refresh-token hash;
- previously consumed refresh-token hashes;
- refresh-token rotation metadata.
The session expiration is bounded and indexed using MongoDB’s TTL functionality.
This gives us a central control point for every authenticated device.
An otherwise valid JWT can now be rejected if its corresponding server-side session:
- does not exist;
- has expired;
- has been revoked;
- belongs to another user;
- is no longer permitted by the current security policy.
Binding access tokens to sessions
JWT access tokens now include a server-generated session identifier in the sid claim.
Conceptually, an access token contains information similar to:
{
  "sub": "user-id",
  "sid": "session-uuid",
  "jti": "token-uuid",
  "iat": 1790540000,
  "exp": 1790540900
}
The authentication middleware performs two checks:
1. verify the JWT signature and expiration;
2. validate the persistent session referenced by sid.
This preserves the performance and interoperability of JWTs while restoring server-side revocation.
If a device is removed from the account-security interface, its access token stops working even if the JWT has not reached its cryptographic expiration.
Rotating opaque refresh tokens
The latest implementation adds rotating refresh tokens.
Refresh tokens are deliberately opaque. They do not contain trusted user information and are not JWTs.
Their format is conceptually:
myzr.<session-id>.<cryptographically-random-secret>
The random secret is generated using a cryptographically secure random-number generator.
The complete refresh token is sent to the browser, but it is never stored in MongoDB.
Instead, the server calculates an HMAC-SHA256 digest:
stored hash = HMAC-SHA256(server secret, raw refresh token)
Only the resulting digest is persisted.
If the database is leaked, an attacker cannot directly use the stored value as a refresh token.
Cookie protections
Refresh tokens are stored in a browser cookie configured with:
HttpOnly
SameSite=Lax
Secure in production
Path=/api/auth
The HttpOnly attribute prevents normal frontend JavaScript from reading the token.
Scoping the cookie to /api/auth reduces the number of requests that carry it.
The access-token cookie follows similar security settings. During the migration period, bearer tokens remain temporarily supported so existing clients do not stop working immediately.
Atomic refresh-token rotation
A refresh token becomes invalid as soon as it is successfully used.
The refresh flow is:
Browser
  |
  | POST /api/auth/refresh
  | HttpOnly refresh cookie
  v
Server
  |
  | Parse session ID
  | Hash presented token
  | Compare it with the current stored hash
  | Generate the next refresh token
  | Atomically replace the stored hash
  v
Browser receives:
  - new access-token cookie
  - new refresh-token cookie
The database update uses an atomic compare-and-swap condition.
The server only rotates the token if all these conditions are still true:
- the session exists;
- the session has not expired;
- the session has not been revoked;
- the submitted token hash matches the current refresh-token hash.
This is important when two refresh requests arrive almost simultaneously. Only one request can successfully consume the current token.
Replay detection
Rotating a refresh token is not enough. The system must also detect when a previously consumed token is used again.
Every consumed refresh-token hash is added to a bounded history attached to the session.
If an old token appears again, the server treats it as a possible credential compromise:
Old refresh token reused
        ↓
Replay detected
        ↓
Entire session revoked
        ↓
Access and refresh cookies cleared
        ↓
User must authenticate again
The session is revoked with the reason:
refresh-token-replay
The history is bounded to the latest 50 hashes so the session document cannot grow indefinitely.
This does not prove exactly how the old token was reused. It could be malicious theft, an outdated client, or an unusual concurrency condition. However, revoking the session is the safest response.
Authentication endpoints
The current security slice exposes the following endpoints.
Refresh the current session
POST /api/auth/refresh
The endpoint:
- reads the HttpOnly refresh cookie;
- rotates the token;
- creates a new access token;
- sets new access and refresh cookies;
- returns public session information;
- never exposes the raw refresh token in the JSON response.
Log out
POST /api/auth/logout
Logout is idempotent.
It clears authentication cookies even when the submitted access token is missing, malformed, expired, or already revoked.
When possible, the refresh token is used to locate and revoke the corresponding server-side session.
Get the authenticated account
GET /api/auth/me
Returns the current authenticated user and safe session information.
List active devices
GET /api/auth/me/sessions
Returns the user’s active sessions and identifies the current device.
Revoke a session
DELETE /api/auth/me/sessions/:sessionId
Allows a user to disconnect another device.
Revoking the current device also clears local authentication state and returns the user to the login flow.
Social authentication
Verified social-login flows now create the same persistent sessions used by password authentication.
After a successful social identity exchange, the server issues:
- a session-bound access token;
- an opaque refresh token;
- the corresponding secure cookies.
The raw refresh token is not inserted into redirect URLs, signed query tickets, or public API responses.
This reduces the possibility of refresh credentials leaking through:
- browser history;
- proxy logs;
- analytics systems;
- referrer headers;
- copied callback URLs.
Account-security interface
The React frontend now includes:
/account/security
The page allows authenticated users to inspect:
- active sessions;
- the current device;
- session creation time;
- last activity;
- expiration;
- device metadata.
Users can revoke another device or terminate the current session.
The page is linked from the authenticated account area and from Neon Plaza.
The Vercel routing configuration also supports the page as a client-side route and applies:
X-Robots-Tag: noindex, nofollow
The security interface should not be indexed by search engines.
Privacy considerations
Raw client IP addresses are not stored in the session collection.
Instead, the backend persists an HMAC-derived digest. This permits limited correlation for security analysis without retaining the original IP address as normal application data.
Device information is also intentionally limited. The system should collect only what is necessary to help a user recognize an active session.
A security feature should not quietly become an invasive tracking system.
Migration strategy
The platform cannot immediately remove every legacy authentication mechanism.
Some clients still expect an access token in localStorage or send it using:
Authorization: Bearer <token>
The current implementation therefore supports a controlled migration period.
Phase 1
Persistent sessions + cookie support + legacy bearer compatibility

Phase 2
Global automatic access-token renewal

Phase 3
CSRF protection and cookie-only authentication

Phase 4
Legacy JWTs and localStorage tokens expire

Phase 5
Enable strict server-session enforcement

Phase 6
Remove the bearer/localStorage migration path
The environment option:
REQUIRE_SERVER_SESSION=true
will enable strict enforcement after the migration window.
It should not be enabled in production until existing clients and legacy tokens have been handled.
Validation completed
The focused implementation has been validated with:
- 30 out of 30 backend tests passing;
- 12 out of 12 frontend tests passing;
- successful React production compilation;
- syntax checks for the modified backend files;
- real MongoDB integration coverage.
The integration test verifies the complete refresh-security flow:
1. create a persistent session;
2. consume the current refresh token;
3. rotate it successfully;
4. reuse the old token;
5. reject the replay;
6. confirm that the session has been revoked;
7. confirm the refresh-token-replay revocation reason.
The relevant authentication suites also pass inside GitHub Actions:
tests/authSessionService.test.js
tests/authSessionController.test.js
tests/socialIdentityService.test.js
Why the complete CI pipeline is still red
The repository-wide test pipeline is not currently green.
At the latest check, the complete backend run reported:
141 test suites passed
42 test suites failed

738 tests passed
44 tests failed
The failing suites are primarily existing failures involving:
- Zorgax payment-intent compatibility;
- Marketplace behavior;
- seller membership and free-listing policies;
- older Metaverse authentication and observability expectations;
- legacy source-code assertion tests.
The Security Audit and MYZ-164 policy workflows are also still failing because of previously recorded repository-wide conditions.
This distinction is important:
The focused MYZ-71 authentication tests are green, but the pull request must not be described as fully CI-clean while unrelated repository failures remain.

The pull request therefore remains in Draft status.
What is still missing
The current implementation is a major security improvement, but it does not complete MYZ-71.
1. Global access-token renewal
The frontend needs a centralized request layer that can:
1. detect an expired access token;
2. call /api/auth/refresh once;
3. retry the original request;
4. prevent multiple simultaneous refresh operations;
5. redirect to login when renewal fails.
Without this integration, individual pages may behave differently when an access token expires.
2. CSRF protection
Moving authentication entirely into cookies changes the threat model.
Before removing bearer-token compatibility, state-changing requests require explicit CSRF protection. Possible controls include:
- strict origin verification;
- CSRF tokens;
- double-submit cookie protection;
- carefully selected SameSite policies;
- rejecting unsafe cross-origin requests.
SameSite=Lax is useful, but it should not be treated as the only CSRF control.
3. Remove localStorage authentication
The frontend refresh client currently updates the legacy browser token as a migration bridge.
The final design should avoid exposing reusable authentication credentials to JavaScript.
Removing localStorage requires:
- migrating all API clients;
- verifying every authenticated route;
- updating websocket or realtime authentication;
- testing mobile and embedded clients;
- allowing existing legacy tokens to expire.
4. Passkeys and magic links
The roadmap still includes passwordless authentication endpoints such as:
/auth/start
/auth/verify
Passkeys should use WebAuthn and require correct configuration of:
- relying-party ID;
- allowed origins;
- challenge generation;
- challenge expiration;
- credential counters;
- credential revocation;
- recovery procedures.
Magic links require single-use, short-lived verification tokens and protection against email-link replay.
5. Step-up authentication
Sensitive actions should require recent stronger authentication.
Possible examples include:
- changing the primary email address;
- changing a password;
- managing passkeys;
- deleting an account;
- accessing payment or wallet operations;
- modifying high-impact Marketplace settings.
A valid long-lived session should not automatically authorize every sensitive operation.
6. Password-change session policy
The product must decide what happens after a password change:
- revoke every session;
- revoke all sessions except the current one;
- let the user choose;
- require step-up authentication first.
This behavior must be enforced server-side and covered by integration tests.
7. Production OAuth validation
Google, GitHub, and Facebook authentication must still be verified with real production configuration:
- exact callback URLs;
- production origins;
- provider secrets;
- redirect allowlists;
- cookie behavior across frontend and API domains;
- account-linking rules;
- missing-email behavior;
- provider-token expiration and error handling.
8. MongoDB Atlas validation
The session schema and indexes must be validated against the production database.
Deployment checks should confirm:
- TTL index creation;
- refresh-token indexes;
- expected session-document growth;
- index compatibility with the existing collection;
- cleanup of expired sessions;
- safe behavior during rolling deployments.
9. Rate limiting and abuse prevention
Authentication endpoints require dedicated limits.
This includes:
- login attempts;
- password reset;
- magic-link generation;
- passkey challenge creation;
- token refresh;
- social-auth callbacks.
Limits should consider both account and privacy-preserving network signals without locking legitimate users out too easily.
10. Security monitoring
The platform needs structured events for:
- successful login;
- failed login;
- session creation;
- session revocation;
- remote-device removal;
- refresh-token replay;
- password changes;
- passkey registration;
- suspicious authentication activity.
Logs must never contain access tokens, refresh tokens, passwords, authorization codes, or raw provider credentials.
Recommended production rollout
The safest rollout sequence is:
1. review the draft pull request;
2. deploy with legacy compatibility enabled;
3. verify MongoDB indexes;
4. validate access and refresh cookies on the public domain;
5. test login, refresh, logout, and remote revocation;
6. deliberately replay an old refresh token in a controlled environment;
7. integrate centralized frontend renewal;
8. implement and test CSRF protection;
9. monitor session and replay events;
10. let legacy credentials expire;
11. enable strict server-session enforcement;
12. remove localStorage and legacy bearer support;
13. add passkeys, magic links, and step-up authentication.
Current status
The current security slice delivers:
Persistent sessions                       ✅
Session-bound access tokens               ✅
Real logout                               ✅
Active-device listing                     ✅
Remote session revocation                 ✅
Rotating opaque refresh tokens            ✅
Refresh-token replay detection            ✅
Password and social-session integration   ✅
Account-security UI                       ✅
Focused backend/frontend validation       ✅

Global access-token renewal               ⏳
CSRF hardening                            ⏳
Cookie-only migration                     ⏳
Removal of localStorage tokens            ⏳
Passkeys and magic links                  ⏳
Step-up authentication                    ⏳
Production OAuth validation               ⏳
Production MongoDB validation             ⏳
Repository-wide green CI                  ⏳
Conclusion
Secure authentication is not a single endpoint or a single token format. It is a lifecycle involving token issuance, renewal, device visibility, revocation, replay detection, privacy, recovery, deployment, and monitoring.
The MyZubster Metaverse now has the foundations for revocable multi-device sessions and secure refresh-token rotation.
The next objective is to complete the browser integration safely: centralized renewal, CSRF protection, cookie-only authentication, and removal of browser-accessible credentials.
Only after those controls are validated should we move to strict session enforcement and production deployment.
The implementation is available in the current draft pull request:
https://github.com/danieldirimini-myzubster/myzubster/pull/10
Enter fullscreen mode Exit fullscreen mode

Top comments (0)