RocketChat/Rocket.Chat · error · Error

error-invalid-token

Error message

error-invalid-token

What it means

verifyEmail throws a plain Error('error-invalid-token') — not a Meteor.Error — at four sites. At line 5-7 the guard is user.services?.email?.verificationTokens?.length === 0: the account has an empty token array, so no verification token was ever issued and any supplied token is meaningless. Quirk: if verificationTokens is undefined (property missing), this guard passes and the failure surfaces at the next one instead.

Source

Thrown at apps/meteor/server/lib/users/verifyEmail.ts:6

import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';

export async function verifyEmail(user: Pick<IUser, '_id' | 'services' | 'emails'>, token: string): Promise<boolean> {
	if (user.services?.email?.verificationTokens?.length === 0) {
		throw new Error('error-invalid-token');
	}

	const tokenRecord = await user.services?.email?.verificationTokens?.find((t) => t.token === token);
	if (!tokenRecord) {
		throw new Error('error-invalid-token');
	}

	const emailsRecord = user.emails?.find((e) => e.address === tokenRecord.address);

	if (!emailsRecord) {
		throw new Error('error-invalid-token');
	}

	const result = await Users.verifyEmailByAddress(user._id, tokenRecord.address);
	if (result.modifiedCount === 0) {
		throw new Error('error-invalid-token');
	}

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Mint a fresh token first: call Accounts.sendVerificationEmail(userId) and verify with the new link
  2. In tests, populate the fixture: services.email.verificationTokens = [{ token, address, when }]
  3. If users are provisioned programmatically, add token issuance to the provisioning script

Example fix

// before
await verifyEmail(user, token); // user has verificationTokens: [] -> error-invalid-token

// after
await Accounts.sendVerificationEmail(user._id);
const fresh = await Users.findOneById(user._id);
const newToken = fresh.services.email.verificationTokens.at(-1).token;
await verifyEmail(fresh, newToken);
Defensive patterns

Strategy: validation

Validate before calling

const tokens = user.services?.email?.verificationTokens;
if (!tokens || tokens.length === 0) {
  // no token to verify against: trigger a resend instead of calling verifyEmail
}

Type guard

const hasVerificationTokens = (u: Pick<IUser, 'services'>): boolean =>
  Array.isArray(u.services?.email?.verificationTokens) && u.services!.email!.verificationTokens.length > 0;

Try / catch

try {
  await verifyEmail(user, token);
} catch (error) {
  if ((error as Error).message === 'error-invalid-token') {
    // note: plain Error, not Meteor.Error — check .message, not .error
  } else {
    throw error;
  }
}

Prevention

When it happens

Trigger: Calling verifyEmail for a user created without verification tokens: admin-created accounts, OAuth-provisioned accounts, users created before Accounts_EmailVerification was enabled, or tokens cleared by a later flow.

Common situations: Custom registration pipelines that skip Accounts.sendVerificationEmail; tests with hand-built user fixtures that have services.email but no verificationTokens array; migrations dropping nested services fields.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/ce659995d1b3f6ae. Report an issue: GitHub.