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
- Enable 'Merge users' (mergeUsers) in Admin -> OAuth -> <custom service> so the SSO identity links into the existing account
- Rename the conflicting local user (Admin -> Users -> edit username) so the OAuth username no longer collides
- Map usernameField to a guaranteed-unique claim such as 'sub' or 'preferred_username' instead of a display name
- 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
- Turn on mergeUsers before rolling SSO out to a workspace that already has password users
- Map username claims to unique identifiers (sub, user_id), never display names
- Decide keyField ('username' vs 'email') up front and keep it stable
- Treat this error as an intentional takeover guard, not a bug - plan account merges during SSO adoption
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.