OrchardCMS/OrchardCore · error · InvalidOperationException

Provider is already linked for

Error message

Provider {login.LoginProvider} is already linked for {user.UserName}

What it means

Thrown by UserStore.AddLoginAsync when the user already has a UserLoginInfo with the same LoginProvider. The store enforces one login entry per external provider per user, preventing duplicate provider links.

Solutions

  1. Check user.LoginInfos (or UserManager.GetLoginsAsync) for the provider before adding.
  2. Catch InvalidOperationException and treat the duplicate link as a no-op/success.
  3. Ensure callback endpoints are idempotent (e.g. redirect after POST, disable double submit).
  4. Deduplicate existing user documents during migration before adding logins.

Example fix

// before
await _userManager.AddLoginAsync(user, info);
// after
var logins = await _userManager.GetLoginsAsync(user);
if (!logins.Any(l => l.LoginProvider == info.LoginProvider))
{
    await _userManager.AddLoginAsync(user, info);
}
Defensive patterns

Strategy: try-catch

Validate before calling

var logins = await _userManager.GetLoginsAsync(user);
bool alreadyLinked = logins.Any(l => l.LoginProvider == info.LoginProvider);

Type guard

bool HasProvider(IEnumerable<UserLoginInfo> logins, string provider) => logins?.Any(l => l.LoginProvider == provider) == true;

Try / catch

try { await _userManager.AddLoginAsync(user, info); }
catch (InvalidOperationException ex) when (ex.Message.Contains("already linked"))
{ _logger.LogInformation("Login already linked for {User}", user.UserName); }

Prevention

When it happens

Trigger: Calling UserManager.AddLoginAsync twice with the same provider for the same user; re-running an external sign-in linking flow; a race where the same association is applied twice.

Common situations: Double-submitting a link-account form; replaying external authentication callbacks; migrating users where provider links were already imported.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13). Data as JSON: /api/errors/6ade7e8d69e769b0. Report an issue: GitHub.

Appendix: source

Thrown at src/OrchardCore/OrchardCore.Users.Core/Services/UserStore.cs:487

        var users = await _session.Query<User, UserByRoleNameIndex>(u => u.RoleName == normalizedRoleName).ListAsync(cancellationToken);
        return users == null ? [] : users.ToList<IUser>();
    }

    #endregion IUserRoleStore<IUser>

    #region IUserLoginStore<IUser>

    public Task AddLoginAsync(IUser user, UserLoginInfo login, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(user);

        ArgumentNullException.ThrowIfNull(login);

        if (user is User u)
        {
            if (u.LoginInfos.Any(i => i.LoginProvider == login.LoginProvider))
            {
                throw new InvalidOperationException($"Provider {login.LoginProvider} is already linked for {user.UserName}");
            }

            u.LoginInfos.Add(login);
        }

        return Task.CompletedTask;
    }

    public async Task<IUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
    {
        return await _session.Query<User, UserByLoginInfoIndex>(u => u.LoginProvider == loginProvider && u.ProviderKey == providerKey).FirstOrDefaultAsync(cancellationToken);
    }

    public Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(user);

        if (user is User u)

View on GitHub (pinned to 4306c0717f)