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
- Check user.LoginInfos (or UserManager.GetLoginsAsync) for the provider before adding.
- Catch InvalidOperationException and treat the duplicate link as a no-op/success.
- Ensure callback endpoints are idempotent (e.g. redirect after POST, disable double submit).
- 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
- Make external-callback linking idempotent
- Prevent double form submission (PRG pattern, disable button)
- Deduplicate before migrating logins
- Check GetLoginsAsync before every AddLoginAsync
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
- Role does not exist.
- Couldn't generate a unique user id. Too many attempts.
- The name cannot be null or empty.
- The value cannot be null or empty.
- code cannot be null or empty.
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)