{"record":{"id":"a9331ab955abd3d5","repo":"RocketChat/Rocket.Chat","slug":"customoauth","errorCode":"CustomOAuth","errorMessage":"User with username ${user.username} already exists","messagePattern":"User with username (.+?) already exists","errorType":"exception","errorClass":"Meteor.Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/lib/auth-providers/custom-oauth/customOAuth.ts","lineNumber":288,"sourceCode":"\t\t\t\t}\n\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tawait callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user });\n\t\t\t\t// User already created or merged and has identical name as before\n\t\t\t\tif (\n\t\t\t\t\tuser.services?.[serviceName as keyof NonNullable<IUser['services']>] &&\n\t\t\t\t\tuser.services[serviceName as keyof NonNullable<IUser['services']>].id === serviceData.id &&\n\t\t\t\t\tuser.name === serviceData.name &&\n\t\t\t\t\t(this.keyField === 'email' || !serviceData.email || user.emails?.find(({ address }) => address === serviceData.email))\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (this.mergeUsers !== true) {\n\t\t\t\t\tthrow new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`);\n\t\t\t\t}\n\n\t\t\t\tconst serviceIdKey = `services.${serviceName}.id`;\n\t\t\t\tconst successCallbacks = [\n\t\t\t\t\tasync () => {\n\t\t\t\t\t\tconst updatedUser = await Users.findOneById(user._id, { projection: { name: 1, emails: 1, [serviceIdKey]: 1 } });\n\t\t\t\t\t\tif (updatedUser) {\n\t\t\t\t\t\t\tconst { _id, ...diff } = updatedUser;\n\t\t\t\t\t\t\tvoid notifyOnUserChange({ clientAction: 'updated', id: user._id, diff });\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t];\n\n\t\t\t\tconst session = client.startSession();\n\t\t\t\ttry {\n\t\t\t\t\t// Extend the session to match the ExtendedSession type expected by saveUserIdentity\n\t\t\t\t\tObject.assign(session, {\n\t\t\t\t\t\tonceSuccesfulCommit: (cb: () => Promise<void>) => {","sourceCodeStart":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/auth-providers/custom-oauth/customOAuth.ts#L270-L306","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: strategy created without mergeUsers -> collision throws\nnew CustomOAuthStrategy('github-enterprise', { serverURL, clientId, clientSecret, ...options });\n\n// after\nnew CustomOAuthStrategy('github-enterprise', { serverURL, clientId, clientSecret, mergeUsers: true, ...options });","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await Accounts.updateOrCreateUserFromExternalService(serviceName, serviceData, options);\n} catch (error) {\n  if (error instanceof Meteor.Error && error.error === 'CustomOAuth' && /already exists/.test(error.reason)) {\n    throw new Meteor.Error('custom-oauth-conflict', 'This username belongs to a local account. Ask an admin to merge or rename it.');\n  }\n  throw error;\n}","preventionTips":["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"],"tags":["oauth","custom-oauth","account-linking","user-conflict","meteor"],"backgroundTag":"oauth-account-linking-conflict","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}