From Stateless JWTs to Revocable Sessions: Securing the MyZubster Metaverse
MyZubster is developing a persistent open-source digital ecosystem that connects verified identities, metaverse characters, community missions, knowledge, marketplaces, and contributor activity.
As the platform grows, authentication can no longer mean simply issuing a JSON Web Token and trusting it until it expires.
A persistent metaverse requires a stronger security model:
- real server-side logout;
- session and device management;
- remote revocation;
- privacy-preserving activity metadata;
- secure browser cookies;
- structured authentication errors;
- compatibility with existing clients;
- a path toward passkeys and rotating refresh tokens.
We have now implemented the first major security slice of MYZ-71 — Authentication, sessions and account security.
The implementation is available in the following draft pull request:
MYZ-71: Add revocable sessions and device security controls
The pull request is still a draft. It has not been merged or deployed to production.
Why stateless JWTs were no longer enough
The previous authentication architecture issued a signed JWT containing information such as:
{
"userId": "USER_ID",
"username": "daniel",
"role": "user",
"iat": 1789970000,
"exp": 1790574800
}
The server could validate the signature and expiration time, but it could not immediately answer several important security questions:
- Did the user log out?
- Was the device remotely revoked?
- Is the token associated with a known active login?
- Was the session disabled after the token was created?
- Which browser created the session?
- Should this token be rejected before its cryptographic expiration?
A purely stateless JWT remains valid until it expires. Logging out in the browser may remove a local token, but it does not necessarily invalidate a copied token.
That limitation becomes especially important when an account controls:
- a persistent metaverse character;
- private rooms;
- creator permissions;
- marketplace activity;
- verified social identities;
- community moderation;
- contributor or reward-related actions.
The new hybrid architecture
We retained JWTs, but every newly issued JWT is now connected to a persistent server-side session.
The new token includes two identifiers:
{
"userId": "USER_ID",
"username": "daniel",
"role": "user",
"sid": "4cb9c3a1-8d39-4f2d-8a91-82c9556abc91",
"jti": "48ab2bd5-b4fa-426a-af42-3e24758e0434",
"iat": 1789970000,
"exp": 1790574800
}
The sid claim identifies the persistent session. The jti uniquely identifies the issued JWT.
Authentication now follows this sequence:
Request
↓
Extract token from secure cookie or Authorization header
↓
Verify JWT signature and expiration
↓
Read the sid claim
↓
Find the corresponding server-side session
↓
Verify ownership, expiration and revocation state
↓
Allow or reject the request
A valid JWT is no longer sufficient when its server-side session has been revoked or expired.
Persistent AuthSession records
A new MongoDB model stores the authentication state.
Conceptually, the model contains:
{
sessionId: String,
userId: ObjectId,
userAgent: String,
ipHash: String,
createdAt: Date,
lastSeenAt: Date,
expiresAt: Date,
revokedAt: Date,
revokedReason: String
}
Session creation
Every password or social login creates a cryptographically random session identifier:
const sessionId = crypto.randomUUID();
A JWT is then signed with that identifier:
const token = jwt.sign(
{
userId: String(user._id),
username: user.username,
role: user.role,
sid: sessionId
},
jwtSecret,
{
expiresIn: sessionTtl,
jwtid: crypto.randomUUID()
}
);
The session document and token are created as part of the same authentication flow.
Expiration enforcement
The session contains an explicit expiration timestamp and a MongoDB TTL index.
However, MongoDB TTL cleanup is asynchronous. An expired document may remain in the collection temporarily.
For this reason, authentication never relies only on physical deletion. The validation query requires:
{
sessionId: decoded.sid,
userId: decoded.userId,
revokedAt: null,
expiresAt: { $gt: new Date() }
}
The session becomes unusable immediately after expiration, even if MongoDB has not deleted it yet.
Revocation
Revocation does not delete the session immediately. Instead, the server records when and why it was revoked:
{
revokedAt: new Date(),
revokedReason: "user-revoked"
}
Keeping this state is useful for security analysis and makes the revocation operation auditable.
Privacy-preserving IP metadata
The server does not store raw IP addresses.
When an address is available, it is converted into an HMAC-SHA256 digest:
const ipHash = crypto
.createHmac("sha256", SESSION_IP_HASH_SECRET)
.update(ipAddress)
.digest("hex");
This allows the application to identify meaningful changes between login contexts without creating a database of clear-text IP addresses.
The HMAC secret prevents the value from behaving like a simple unsalted hash that could be easily precomputed.
Last-activity tracking without excessive writes
The session records lastSeenAt, but it is not updated on every request.
Updating the database for every authenticated API call would create unnecessary write volume.
Instead, the timestamp is refreshed only when the previous update is older than five minutes:
if (Date.now() - lastSeenAt > FIVE_MINUTES) {
await AuthSession.updateOne(
{sessionId: decoded.sid,
revokedAt: null
},
{
$set: {
lastSeenAt: new Date()
}
}
);
}
This provides useful account activity information while reducing database pressure.
Secure cookies and compatibility mode
The authentication token can now be received from either:
Authorization: Bearer TOKEN
or the following browser cookie:
myzubster_session
The cookie uses these properties:
{
httpOnly: true,
secure: production,
sameSite: "lax",
path: "/",
maxAge: sessionTtl
}
Why use HttpOnly?
Browser JavaScript cannot directly read an HttpOnly cookie.
This limits simple token extraction through code such as:
document.cookie
It does not solve every XSS scenario, but it substantially reduces the exposure of authentication secrets.
Why bearer tokens are still supported
MyZubster already has clients that use tokens stored by older authentication flows.
Removing bearer support immediately would invalidate existing sessions and potentially break multiple parts of the frontend.
The system therefore supports a migration period:
Legacy client
→ Authorization: Bearer
New browser session
→ HttpOnly myzubster_session cookie
Older JWTs may also lack an sid claim. They remain temporarily compatible unless strict mode is enabled:
REQUIRE_SERVER_SESSION=true
Once existing tokens have expired, strict mode can reject every JWT that is not connected to a server session.
New authentication endpoints
Four important endpoints were added.
Current account
GET /api/auth/me
Returns the authenticated user and associated public character information.
Active devices and sessions
GET /api/auth/me/sessions
Example response:
{
"success": true,
"request_id": "bf7a957e-1669-4432-a9a4-e686cb70cc07",
"sessions": [
{
"id": "4cb9c3a1-8d39-4f2d-8a91-82c9556abc91",
"current": true,
"device": "Mozilla/5.0 ...",
"createdAt": "2026-09-27T08:00:00.000Z",
"lastSeenAt": "2026-09-27T09:00:00.000Z",
"expiresAt": "2026-10-04T08:00:00.000Z"
}
]
}
Only sessions belonging to the authenticated user are returned.
Revoke a session
DELETE /api/auth/me/sessions/:sessionId
The update is scoped by both the user and session identifier:
{
userId: authenticatedUserId,
sessionId,
revokedAt: null
}
This prevents one account from revoking another user’s session.
Logout
POST /api/auth/logout
Logout is deliberately idempotent.
The cookie is cleared even when the submitted token is:
- expired;
- malformed;
- already revoked;
- disconnected from a server session.
The logout route is intentionally not protected by the standard authentication middleware. Otherwise, an expired token could be rejected before the handler had an opportunity to clear the invalid cookie.
When a valid session is available, logout also revokes it server-side.
Structured authentication errors
Authentication errors now use stable public codes instead of exposing raw JWT library messages.
Example:
{
"success": false,
"request_id": "5fe11522-c804-4907-a4d1-0f459f94ab2a",
"error": {
"code": "AUTH_SESSION_REVOKED",
"message": "Session expired or revoked"
}
}
Current error categories include:
AUTH_TOKEN_MISSING
AUTH_TOKEN_INVALID
AUTH_TOKEN_EXPIRED
AUTH_SESSION_REVOKED
Each response contains a request ID that can be correlated with server logs.
This is useful for debugging while avoiding the disclosure of stack traces, signing details, or internal JWT parsing errors.
Integration with social authentication
Persistent sessions are now created for:
- registration;
- password login;
- Google login;
- GitHub login;
- Facebook login.
The social OAuth ticket exchange now creates the same server-side session used by password authentication and sets the secure cookie.
During integration testing, we also corrected two existing issues.
GitHub profile persistence
An undefined nested GitHub public-profile snapshot could cause a Mongoose casting error.
The new implementation updates nested data explicitly and writes the public snapshot only when a defined value is available.
Facebook accounts without email
Not every Facebook account exposes an email address.
MyZubster already supported a deterministic private relay identity for this situation. The integration tests were aligned with that intended behavior instead of incorrectly requiring every account to provide an email.
The relay value is treated as an internal identifier, not as a verified personal email.
The account-security interface
A new React page is available at:
/account/security
It displays:
- the authenticated account;
- all active devices;
- the current device;
- session creation time;
- last activity;
- session expiration;
- controls for remote revocation;
- current-session termination;
- logout.
The page is linked from:
- the authenticated account panel on the MyZubster home interface;
- the Neon Plaza metaverse top bar.
Frontend API module
The React application uses a dedicated session API:
getCurrentAccount();
getAuthSessions();
revokeAuthSession(sessionId);
logoutCurrentSession();
clearBrowserAuth();
Requests include same-origin cookies:
fetch(path, {
credentials: "same-origin",
headers: {
...bearerMigrationFallback
}
});
Structured server errors are preserved so the UI can display a support request ID when an operation fails.
Remote device revocation
When the user revokes another device:
- the UI requests confirmation;
- a session-specific DELETE request is sent;
- the server verifies session ownership;
- the session receives a revocation timestamp;
- the frontend removes it from the active list;
- the revoked device is rejected on its next request.
Current-device termination
Terminating the current session:
- revokes the server-side session;
- clears the secure cookie;
- removes legacy browser credentials;
- redirects the user through authentication again.
Legacy browser keys removed during logout include:
myzubster-token
token
accessToken
myzubster-user
myzubster-identity-provider
myzubster-metaverse-character-id
Protected production routing
The new account page is configured as an explicit Vercel SPA route.
It includes:
X-Robots-Tag: noindex, nofollow
Account-security pages should not be indexed, cached as search snippets, or treated as public content.
Both the root and standalone frontend Vercel configurations were updated and validated as JSON.
Testing and verification
Backend
Five focused backend suites pass:
authSessionService
authMiddlewareSessions
authSessionController
socialAuthCallback
socialIdentityService
Results:
5 suites passed
24 tests passed
Coverage includes:
- persistent session creation;
- bearer and cookie extraction;
- active-session validation;
- revoked-session rejection;
- token-expiration handling;
- structured errors;
- session listing;
- remote revocation;
- valid logout;
- malformed-token logout;
- Google authentication;
- GitHub authentication;
- Facebook authentication;
- persistent metaverse characters.
The social identity tests use MongoDB Memory Server so the persistence behavior is exercised rather than completely mocked.
Frontend
Three focused frontend suites pass:
authSessions API
AccountSecurityPage
MetaversePage
Results:
3 suites passed
11 tests passed
The tests cover:
- same-origin credentials;
- bearer-token migration;
- safe session-ID encoding;
- structured server errors;
- local credential cleanup;
- account and session rendering;
- current-device labeling;
- remote revocation;
- removal of revoked devices;
- safe date formatting;
- existing Neon Plaza mission-state behavior.
The React production bundle also compiles successfully.
Repository-wide CI status
The pull request remains a draft because repository-wide CI still contains failures outside this authentication change.
A previous complete run reported:
149 test suites passed
34 test suites failed
774 tests passed
44 tests failed
The sampled failures were primarily related to existing:
Frontend JavaScript:
no direct access to reusable authentication secrets
Once the migration period ends, MyZubster should stop returning long-lived authentication tokens to frontend JavaScript.
Production validation
The following checks remain:
- secure cookie verification on the public HTTPS domain;
- remote revocation between two physical devices;
- GitHub OAuth callback verification in production;
- Google and Facebook regression checks;
- MongoDB TTL-index verification;
- investigation of intermittent Atlas ReplicaSetNoPrimary events;
- strict-session migration testing;
- request-ID propagation through production logs and proxies.
Why this is important for the metaverse
Authentication is not separate from the metaverse. It is the foundation of persistent identity.
The MyZubster identity chain is becoming:
MyZubster account
↓
Revocable server session
↓
Verified character
↓
Neon Plaza participation
↓
Missions and contributions
↓
Knowledge, marketplace and community activity
If a token is stolen, revocation must also protect the connected metaverse character and its permissions.
This session infrastructure prepares the platform for:
- private metaverse rooms;
- verified contributors;
- creator permissions;
- moderation roles;
- university pilots;
- community governance;
- marketplace actions;
- development requests;
- contribution evidence.
Current status
Implemented:
- persistent authentication sessions;
- JWT-to-session binding;
- secure cookies;
- bearer-token migration;
- session expiration;
- device listing;
- remote revocation;
- server-side logout;
- privacy-preserving IP hashing;
- structured errors and request IDs;
- social-login integration;
- account-security UI;
- Neon Plaza integration;
- protected Vercel routing;
- focused backend and frontend tests.
Still required:
- refresh-token rotation;
- replay detection;
- passkeys;
- magic links;
- step-up authentication;
- CSRF hardening;
- removal of browser-readable credentials;
- production OAuth verification;
- Atlas stability verification;
- repository-wide CI cleanup.
This is an important security milestone, but it is not a declaration that authentication is finished.
The goal is to make every security claim testable, observable and reversible during rollout.
In MyZubster, trust should not depend on promises. It should be built through verifiable behavior.
Suggested Coder Legion tags:
Cybersecurity
Node.js
React
Web Development
Authentication
Metaverse
Suggested excerpt:
How MyZubster is moving from stateless JWT authentication to persistent, revocable sessions with secure cookies, device management, structured errors, social-login integration and a React account-security dashboard.