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
- Enable 'Merge users' (mergeUsers option) in the custom OAuth configuration so the identity links into the existing account
- Rename the conflicting local user so the OAuth username no longer collides
- Map usernameField to a unique claim (sub, preferred_username) and set keyField deliberately
- 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
- Enable mergeUsers before SSO rollout on workspaces with existing password users
- Map usernameField to unique claims (sub, preferred_username)
- Keep keyField stable once chosen
- Use the error as a signal to run an account-merge cleanup, not to disable SSO
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
- CustomOAuth
- CustomOAuth: Failed to extract avatar url
- field_not_found
- CustomOAuth: Failed to extract username
- CustomOAuth: Failed to extract email
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/2efc7817ab2e2371.
Report an issue: GitHub.