Description:
POST /api/v1/users.createToken returns HTTP 400 with {"success":false,"error":"Cannot read properties of undefined (reading 'tokens')"} whenever the target user was just created via the admin API and has not yet performed a password login. The handler's User.ensureLoginTokensLimit runs an aggregation over services.resume.loginTokens that returns [] for these users, and the destructure const [{ tokens }] = await Users.findAllResumeTokensByUserId(uid) then yields tokens = undefined → the next tokens.length access throws.
This is a regression versus 6.10.10 — we're hitting it after upgrading from 6.10.10 → 7.13.7 in an environment where backend services routinely use users.createToken to mint tokens for freshly-provisioned users (e.g., on-prem auto-login after KC-driven registration, where the user has an admin-set random password and has never signed in via /api/v1/login themselves).
Steps to reproduce:
- Start a clean RC 7.13.7 instance. Set
CREATE_TOKENS_FOR_USERS=true in the environment.
- Grant the calling user the
admin role (so they have user-generate-access-token).
- As an admin, create a fresh user via
POST /api/v1/users.create:
{
"username": "freshuser",
"email": "freshuser@example.com",
"name": "freshuser",
"password": "<some-random-uuid>",
"verified": true,
"joinDefaultChannels": false,
"requirePasswordChange": false
}
- Immediately (the user has not yet logged in) call:
POST /api/v1/users.createToken
X-Auth-Token / X-User-Id: <admin>
{ "userId": "<the new user's _id>" }
Expected behavior:
users.createToken returns 200 with a valid authToken/userId. The user's services.resume.loginTokens ends up with at least the one entry that was just inserted.
Actual behavior:
HTTP/1.1 400 Bad Request
{
"success": false,
"error": "Cannot read properties of undefined (reading 'tokens')"
}
The freshly created user has services.password.bcrypt set but services.resume is undefined (the admin users.create path does not seed services.resume.loginTokens for users that have never logged in).
Root cause (server-side code excerpt)
In apps/meteor/app/lib/server/lib/User.ts (or wherever ensureLoginTokensLimit lives — visible in the published app.js bundle at /app/bundle/programs/server/app/app.js):
async ensureLoginTokensLimit(uid) {
const [{ tokens }] = await Users.findAllResumeTokensByUserId(uid);
if (tokens.length < getMaxLoginTokens()) {
return;
}
const oldestDate = tokens.reverse()[getMaxLoginTokens() - 1];
await Users.removeOlderResumeTokensByUserId(uid, oldestDate.when);
}
The aggregation in Users.findAllResumeTokensByUserId (in @rocket.chat/models/dist/models/Users.js):
findAllResumeTokensByUserId(userId) {
return this.col
.aggregate([
{ $match: { _id: userId } },
{
$project: {
tokens: {
$filter: {
input: '$services.resume.loginTokens',
as: 'token',
cond: { $ne: ['$$token.type', 'personalAccessToken'] },
},
},
},
},
{ $unwind: '$tokens' },
{ $sort: { 'tokens.when': 1 } },
{ $group: { _id: '$_id', tokens: { $push: '$tokens' } } },
])
.toArray();
}
When services.resume.loginTokens is missing on the user document, $filter returns null, the subsequent $unwind: '$tokens' drops the document (default behaviour without preserveNullAndEmptyArrays), and the pipeline returns []. Then [{ tokens }] = [] destructures undefined, and tokens.length throws the V8 error visible in the API response.
generateAccessToken (the users.createToken handler in app/api/server/v1/users.ts → published app.js):
async function generateAccessToken(callee, userId) {
if (!['yes', 'true'].includes(String(process.env.CREATE_TOKENS_FOR_USERS)) ||
(callee !== userId && !(await hasPermissionAsync(callee, 'user-generate-access-token')))) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'createToken' });
}
const token = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(userId, token); // ← no await
await User.ensureLoginTokensLimit(userId); // ← runs immediately
return { userId, authToken: token.token };
}
Accounts._insertLoginToken returns a Promise in Meteor 3.x but is called without await, so ensureLoginTokensLimit queries Mongo while the insert is still in flight. Even when the insert eventually completes the request has already failed.
Suggested fix
Either:
-
await Accounts._insertLoginToken(userId, token); in generateAccessToken, so the loginTokens array exists by the time ensureLoginTokensLimit reads it. (Cleanest. The Promise is not awaited today, which is the actual race.)
-
Make ensureLoginTokensLimit resilient to an empty aggregation result, e.g.:
const result = await Users.findAllResumeTokensByUserId(uid);
const tokens = result.length > 0 ? result[0].tokens ?? [] : [];
if (tokens.length < getMaxLoginTokens()) return;
Either change unblocks admin-driven users.createToken calls for fresh users.
Server Setup Information:
- Version of Rocket.Chat Server: 7.13.7
- License Type: Community
- Number of Users: ~hundreds (tenant-scoped)
- Operating System: Linux (kubernetes pod)
- Deployment Method: docker (image
rocketchat/rocket.chat:7.13.7) via Helm
- Number of Running Instances: 1
- DB Replicaset Oplog: enabled
- NodeJS Version: v22.16.0
- MongoDB Version: v6.0.27
Client Setup Information
- N/A — the call is server-to-server via REST API from a Java backend (Spring
RestTemplate).
Additional context
We hit this in an SSO-driven registration flow where a Java backend (userService) provisions an RC account on registration via users.create with a random password, then immediately calls users.createToken to obtain a session token for the user (the user never logs into RC via password directly — they authenticate at the IdP). On RC 6.10.10 this works; on 7.13.7 every registration produces this 400.
We've shipped a server-side workaround in our backend that detects the marker string and falls back to admin users.update (password reset) + POST /api/v1/login (which goes through the regular Meteor Accounts path and seeds services.resume.loginTokens correctly). After the first login the user has loginTokens and subsequent users.createToken calls work. Filing this upstream so the workaround can eventually be removed.
Relevant logs:
From the calling backend (HTTP-level):
400 Bad Request on POST request for "http://rocketchat.develop:3000/api/v1/users.createToken":
"{"success":false,"error":"Cannot read properties of undefined (reading 'tokens')"}"
The RC server-side logs at level: 40 (warn) do not surface this; the failing request goes out as 400 without a corresponding server log entry at warn/error level in our deployment. Bumping log level would presumably expose the JS stack trace.
Description:
POST /api/v1/users.createTokenreturns HTTP 400 with{"success":false,"error":"Cannot read properties of undefined (reading 'tokens')"}whenever the target user was just created via the admin API and has not yet performed a password login. The handler'sUser.ensureLoginTokensLimitruns an aggregation overservices.resume.loginTokensthat returns[]for these users, and the destructureconst [{ tokens }] = await Users.findAllResumeTokensByUserId(uid)then yieldstokens = undefined→ the nexttokens.lengthaccess throws.This is a regression versus 6.10.10 — we're hitting it after upgrading from 6.10.10 → 7.13.7 in an environment where backend services routinely use
users.createTokento mint tokens for freshly-provisioned users (e.g., on-prem auto-login after KC-driven registration, where the user has an admin-set random password and has never signed in via/api/v1/loginthemselves).Steps to reproduce:
CREATE_TOKENS_FOR_USERS=truein the environment.adminrole (so they haveuser-generate-access-token).POST /api/v1/users.create:{ "username": "freshuser", "email": "freshuser@example.com", "name": "freshuser", "password": "<some-random-uuid>", "verified": true, "joinDefaultChannels": false, "requirePasswordChange": false }Expected behavior:
users.createTokenreturns 200 with a validauthToken/userId. The user'sservices.resume.loginTokensends up with at least the one entry that was just inserted.Actual behavior:
The freshly created user has
services.password.bcryptset butservices.resumeis undefined (the adminusers.createpath does not seedservices.resume.loginTokensfor users that have never logged in).Root cause (server-side code excerpt)
In
apps/meteor/app/lib/server/lib/User.ts(or whereverensureLoginTokensLimitlives — visible in the publishedapp.jsbundle at/app/bundle/programs/server/app/app.js):The aggregation in
Users.findAllResumeTokensByUserId(in@rocket.chat/models/dist/models/Users.js):When
services.resume.loginTokensis missing on the user document,$filterreturnsnull, the subsequent$unwind: '$tokens'drops the document (default behaviour withoutpreserveNullAndEmptyArrays), and the pipeline returns[]. Then[{ tokens }] = []destructuresundefined, andtokens.lengththrows the V8 error visible in the API response.generateAccessToken(theusers.createTokenhandler inapp/api/server/v1/users.ts→ publishedapp.js):Accounts._insertLoginTokenreturns a Promise in Meteor 3.x but is called withoutawait, soensureLoginTokensLimitqueries Mongo while the insert is still in flight. Even when the insert eventually completes the request has already failed.Suggested fix
Either:
await Accounts._insertLoginToken(userId, token);ingenerateAccessToken, so the loginTokens array exists by the timeensureLoginTokensLimitreads it. (Cleanest. The Promise is not awaited today, which is the actual race.)Make
ensureLoginTokensLimitresilient to an empty aggregation result, e.g.:Either change unblocks admin-driven
users.createTokencalls for fresh users.Server Setup Information:
rocketchat/rocket.chat:7.13.7) via HelmClient Setup Information
RestTemplate).Additional context
We hit this in an SSO-driven registration flow where a Java backend (userService) provisions an RC account on registration via
users.createwith a random password, then immediately callsusers.createTokento obtain a session token for the user (the user never logs into RC via password directly — they authenticate at the IdP). On RC 6.10.10 this works; on 7.13.7 every registration produces this 400.We've shipped a server-side workaround in our backend that detects the marker string and falls back to admin
users.update(password reset) +POST /api/v1/login(which goes through the regular Meteor Accounts path and seedsservices.resume.loginTokenscorrectly). After the first login the user has loginTokens and subsequentusers.createTokencalls work. Filing this upstream so the workaround can eventually be removed.Relevant logs:
From the calling backend (HTTP-level):
The RC server-side logs at
level: 40(warn) do not surface this; the failing request goes out as 400 without a corresponding server log entry at warn/error level in our deployment. Bumping log level would presumably expose the JS stack trace.