{"record":{"id":"e617ea03d515a455","repo":"SonarSource/sonarqube","slug":"user-matched-by-external-login-but-the-store","errorCode":null,"errorMessage":"User '{}' matched by external login, but the stored external ID differs - possible recycled external username","messagePattern":"User '(.+?)' matched by external login, but the stored external ID differs - possible recycled external username","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistrarImpl.java","lineNumber":160,"sourceCode":"\n  private static void validateEmailToAvoidLoginRecycling(UserIdentity userIdentity, UserDto user, Source source) {\n    String dbEmail = user.getEmail();\n\n    if (dbEmail == null) {\n      return;\n    }\n\n    String externalEmail = userIdentity.getEmail();\n\n    if (!dbEmail.equalsIgnoreCase(externalEmail)) {\n      LOGGER.warn(\"User with login '{}' tried to login with email '{}' which doesn't match the email on record '{}'\", userIdentity.getProviderLogin(), externalEmail, dbEmail);\n      throw failAuthenticationException(userIdentity, source);\n    }\n  }\n\n  private static void validateExternalIdToAvoidLoginRecycling(UserIdentity userIdentity, UserDto user, Source source) {\n    if (!userExternalIdMatchesLogin(user)) {\n      LOGGER.warn(\"User '{}' matched by external login, but the stored external ID differs - possible recycled external username\", userIdentity.getProviderLogin());\n      throw failAuthenticationException(userIdentity, source);\n    }\n  }\n\n  private static boolean userExternalIdMatchesLogin(UserDto user) {\n    return Objects.equals(user.getExternalId(), user.getExternalLogin());\n  }\n\n  private static AuthenticationException failAuthenticationException(UserIdentity userIdentity, Source source) {\n    String message = String.format(\"Failed to authenticate with login '%s'\", userIdentity.getProviderLogin());\n    return authException(userIdentity, source, message, message);\n  }\n\n  private static AuthenticationException authException(UserIdentity userIdentity, Source source, String message, String publicMessage) {\n    return AuthenticationException.newBuilder()\n      .setSource(source)\n      .setLogin(userIdentity.getProviderLogin())\n      .setMessage(message)","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/SonarSource/sonarqube/blob/184c821202192afc1c599fc912d0889b69fffa53/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistrarImpl.java#L142-L178","documentation":"UserRegistrarImpl.validateExternalIdToAvoidLoginRecycling throws when a user matched by external login (provider username) has a stored external ID that differs from the ID the identity provider now reports. userExternalIdMatchesLogin compares user.getExternalId() with user.getExternalLogin(); when they diverge, the provider login was likely recycled or reassigned, and authentication is aborted to prevent account takeover. The warning log names the provider login before failAuthenticationException is thrown.","triggerScenarios":"During ALM authentication, validateAlmSpecificData -> validateExternalIdToAvoidLoginRecycling finds an existing UserDto whose externalLogin matches the incoming provider login but whose externalId (stable provider user id) does not equal that login string.","commonSituations":"Identity provider deleted and recreated a user with the same username but a different numeric ID; SonarQube was migrated from a setup where externalId was stored differently (e.g. login instead of numeric GitHub id); provisioning tooling changed how externalId is populated.","solutions":["Verify at the identity provider whether the account was deleted/recreated; if so, update or delete the stale SonarQube user so a fresh provisioning stores the new external ID.","Correct the user's externalId/externalLogin data (update web service or SQL via support guidance) so externalId matches the provider's current identifier.","If migration caused the divergence, re-run the identity sync/provisioning to repopulate external IDs from the provider.","If this is an actual account takeover attempt, keep the block and investigate the provider account activity."],"exampleFix":"// before: sonar user 'jdoe' externalId='jdoe' (legacy), GitHub now reports externalId='1234567'\n// after: re-provision or correct so externalId == externalLogin-based match passes\nUsers > jdoe > delete stale account; user logs in again via GitHub; new record created with externalId=1234567","handlingStrategy":"validation","validationCode":"// Before reusing a provider login, verify stored identifiers\nboolean safe = Objects.equals(user.getExternalId(), user.getExternalLogin());\nif (!safe) { reProvisionUser(user.getLogin()); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never delete-and-recreate provider accounts with the same username; expect the external ID to change.","After IdP migrations, run a re-provisioning/sync so SonarQube external IDs are refreshed.","Monitor this warning — it may indicate an attempted account takeover; investigate rather than blindly 'fixing' data."],"tags":["authentication","login-recycling","external-id-mismatch","account-takeover"],"backgroundTag":"authentication-required","analyzedSha":"184c821202192afc1c599fc912d0889b69fffa53","analyzedAt":"2026-09-09T12:23:51.573Z","contentChangedAt":"2026-09-09T12:23:51.573Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}