SonarSource/sonarqube · error

User '{}' matched by external login, but the stored external

Error message

User '{}' matched by external login, but the stored external ID differs - possible recycled external username

What it means

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.

Source

Thrown at server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistrarImpl.java:160

  private static void validateEmailToAvoidLoginRecycling(UserIdentity userIdentity, UserDto user, Source source) {
    String dbEmail = user.getEmail();

    if (dbEmail == null) {
      return;
    }

    String externalEmail = userIdentity.getEmail();

    if (!dbEmail.equalsIgnoreCase(externalEmail)) {
      LOGGER.warn("User with login '{}' tried to login with email '{}' which doesn't match the email on record '{}'", userIdentity.getProviderLogin(), externalEmail, dbEmail);
      throw failAuthenticationException(userIdentity, source);
    }
  }

  private static void validateExternalIdToAvoidLoginRecycling(UserIdentity userIdentity, UserDto user, Source source) {
    if (!userExternalIdMatchesLogin(user)) {
      LOGGER.warn("User '{}' matched by external login, but the stored external ID differs - possible recycled external username", userIdentity.getProviderLogin());
      throw failAuthenticationException(userIdentity, source);
    }
  }

  private static boolean userExternalIdMatchesLogin(UserDto user) {
    return Objects.equals(user.getExternalId(), user.getExternalLogin());
  }

  private static AuthenticationException failAuthenticationException(UserIdentity userIdentity, Source source) {
    String message = String.format("Failed to authenticate with login '%s'", userIdentity.getProviderLogin());
    return authException(userIdentity, source, message, message);
  }

  private static AuthenticationException authException(UserIdentity userIdentity, Source source, String message, String publicMessage) {
    return AuthenticationException.newBuilder()
      .setSource(source)
      .setLogin(userIdentity.getProviderLogin())
      .setMessage(message)

View on GitHub (pinned to 184c821202)

Solutions

  1. 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.
  2. Correct the user's externalId/externalLogin data (update web service or SQL via support guidance) so externalId matches the provider's current identifier.
  3. If migration caused the divergence, re-run the identity sync/provisioning to repopulate external IDs from the provider.
  4. If this is an actual account takeover attempt, keep the block and investigate the provider account activity.

Example fix

// before: sonar user 'jdoe' externalId='jdoe' (legacy), GitHub now reports externalId='1234567'
// after: re-provision or correct so externalId == externalLogin-based match passes
Users > jdoe > delete stale account; user logs in again via GitHub; new record created with externalId=1234567
Defensive patterns

Strategy: validation

Validate before calling

// Before reusing a provider login, verify stored identifiers
boolean safe = Objects.equals(user.getExternalId(), user.getExternalLogin());
if (!safe) { reProvisionUser(user.getLogin()); }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of SonarSource/sonarqube@184c821202 (2026-09-09). Data as JSON: /api/errors/e617ea03d515a455. Report an issue: GitHub.