RocketChat / RocketChat/Rocket.Chat
users.createToken: "Cannot read properties of undefined (reading 'tokens')" in ensureLoginTokensLimit for admin-created users that have never logged in
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 46.1k
- Forks
- 13.9k
- Avg merge
- 3d 3h
- Merged PRs (30d)
- 130
Description
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=truein the environment. - Grant the calling user the
adminrole (so they haveuser-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);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.: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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with generateAccessToken in app/api/server/v1/users.ts and ensureLoginTokensLimit in apps/meteor/app/lib/server/lib/User.ts, then inspect Users.findAllResumeTokensByUserId in @rocket.chat/models/dist/models/Users.js. Reproduce the admin-created user flow and verify that users.createToken returns 200 with a valid token when the user has never logged in.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- api, authentication, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100