RocketChat/Rocket.Chat · error · Meteor.Error

CustomOAuth

CustomOAuth

Error message

User with username ${user.username} already exists

What it means

Legacy (deprecated, non-Passport) CustomOAuth class twin of the account-collision error: the pre-login hook found an existing user whose username (keyField 'username') or e-mail (keyField 'email') matches the OAuth identity, the user is not already linked to this service id (or their data changed), and mergeUsers !== true. Thrown as Meteor.Error('CustomOAuth', 'User with username <u> already exists') to prevent an SSO identity from silently taking over a local account.

Source

Thrown at apps/meteor/server/lib/auth-providers/custom-oauth/custom_oauth_server.js:416

				if (!user) {
					return;
				}

				await callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user });

				// User already created or merged and has identical name as before
				if (
					user.services &&
					user.services[serviceName] &&
					user.services[serviceName].id === serviceData.id &&
					user.name === serviceData.name &&
					(this.keyField === 'email' || !serviceData.email || user.emails?.find(({ address }) => address === serviceData.email))
				) {
					return;
				}

				if (this.mergeUsers !== true) {
					throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`);
				}

				const serviceIdKey = `services.${serviceName}.id`;
				const successCallbacks = [
					async () => {
						const updatedUser = await Users.findOneById(user._id, { projection: { name: 1, emails: 1, [serviceIdKey]: 1 } });
						if (updatedUser) {
							const { _id, ...diff } = updatedUser;
							void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff });
						}
					},
				];

				const session = client.startSession();
				try {
					// Extend the session to match the ExtendedSession type expected by saveUserIdentity
					Object.assign(session, {
						onceSuccesfulCommit: (cb) => {

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Enable 'Merge users' (mergeUsers option) in the custom OAuth configuration so the identity links into the existing account
  2. Rename the conflicting local user so the OAuth username no longer collides
  3. Map usernameField to a unique claim (sub, preferred_username) and set keyField deliberately
  4. Enable mergeUsersDistinctServices when multiple providers legitimately share usernames

Example fix

// before
new CustomOAuth('keycloak', { serverURL, mergeUsers: false, ...opts });
// -> Meteor.Error CustomOAuth: User with john.doe already exists

// after
new CustomOAuth('keycloak', { serverURL, mergeUsers: true, ...opts });
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await Accounts.updateOrCreateUserFromExternalService(serviceName, serviceData, options);
} catch (error) {
  if (error instanceof Meteor.Error && error.error === 'CustomOAuth' && /already exists/.test(error.reason)) {
    throw new Meteor.Error('custom-oauth-conflict', 'Username already taken by a local account; merge or rename required.');
  }
  throw error;
}

Prevention

When it happens

Trigger: keyField 'username' and the OAuth username matches a pre-existing local user not yet linked to this service; keyField 'email' and the OAuth e-mail matches another account; a returning linked user whose provider-side name changed so the 'identical data' early-return no longer applies while mergeUsers is disabled.

Common situations: Password-registered users trying SSO for the first time after SSO rollout; two IdPs sharing usernames/e-mails; admin never enabled 'Merge users'; usernameField mapped to a non-unique claim.

Related errors


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