RocketChat/Rocket.Chat · error · Meteor.Error

CustomOAuth

CustomOAuth

Error message

User with username ${user.username} already exists

What it means

Thrown by the custom OAuth pre-login hook (BeforeUpdateOrCreateUserFromExternalService) when the identity produced by the provider maps onto an existing Rocket.Chat user - same username (keyField 'username') or same e-mail (keyField 'email') - but that user is not already linked to this service id (or their name/e-mail changed), and the strategy was not configured with mergeUsers: true. It is an intentional takeover guard: an OAuth identity must not silently absorb a locally registered account.

Source

Thrown at apps/meteor/server/lib/auth-providers/custom-oauth/customOAuth.ts:288

				}

				if (!user) {
					return;
				}

				await callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user });
				// User already created or merged and has identical name as before
				if (
					user.services?.[serviceName as keyof NonNullable<IUser['services']>] &&
					user.services[serviceName as keyof NonNullable<IUser['services']>].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: () => Promise<void>) => {

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Enable 'Merge users' (mergeUsers) in Admin -> OAuth -> <custom service> so the SSO identity links into the existing account
  2. Rename the conflicting local user (Admin -> Users -> edit username) so the OAuth username no longer collides
  3. Map usernameField to a guaranteed-unique claim such as 'sub' or 'preferred_username' instead of a display name
  4. Set keyField explicitly ('username' or 'email') so the lookup matches how your users actually collide, and enable mergeUsersDistinctServices when several providers share usernames

Example fix

// before: strategy created without mergeUsers -> collision throws
new CustomOAuthStrategy('github-enterprise', { serverURL, clientId, clientSecret, ...options });

// after
new CustomOAuthStrategy('github-enterprise', { serverURL, clientId, clientSecret, mergeUsers: true, ...options });
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', 'This username belongs to a local account. Ask an admin to merge or rename it.');
  }
  throw error;
}

Prevention

When it happens

Trigger: keyField 'username' and the OAuth username claim equals an existing local user's username while services.<name>.id does not match; keyField 'email' and the OAuth e-mail matches another account's address; a second custom OAuth provider returns the same username as the first; the linked user changed their display name on the provider so the 'identical data' early-return no longer applies and mergeUsers is false.

Common situations: Employees registered with password login before SSO was introduced, then try SSO with the same username; two providers (Google plus a custom IdP) expose the same e-mail; admin left 'Merge users' disabled in the custom OAuth settings; usernameField mapped to a non-unique claim like a first name.

Related errors


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