Kareadita/Kavita · error · KavitaException

errors.oidc.email-in-use

Error message

errors.oidc.email-in-use

What it means

Thrown when an OIDC login matches an existing Kavita user by email, but that user already has a non-empty OidcId — i.e. the email belongs to an account already linked to a different OIDC identity. Kavita blocks the takeover: it won't reassign the account to a new oidcId. This protects against email reuse after an IdP-side email change.

Source

Thrown at Kavita.Services/OidcService.cs:103

        if (string.IsNullOrEmpty(email))
        {
            throw new KavitaException("errors.oidc.missing-email");
        }

        if (settings.RequireVerifiedEmail && !principal.HasVerifiedEmail())
        {
            throw new KavitaException("errors.oidc.email-not-verified");
        }


        user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct);
        if (user != null)
        {
            // Don't allow taking over accounts
            // This could happen if the user changes their email in OIDC, and then someone else uses the old one
            if (!string.IsNullOrEmpty(user.OidcId))
            {
                throw new KavitaException("errors.oidc.email-in-use");
            }

            logger.LogDebug("User {UserName} has matched on email to {OidcId}", user.Id, oidcId);
            user.OidcId = oidcId;
            await unitOfWork.CommitAsync(ct);

            await SyncUserSettings(request, settings, principal, user);

            return user;
        }

        return await CreateNewAccount(request, principal, settings, oidcId);
    }

    public async Task<AppUser?> RefreshCookieToken(CookieValidatePrincipalContext ctx, CancellationToken ct = default)
    {
        if (ctx.Principal == null) return null;

View on GitHub (pinned to 9c3e540000)

Solutions

  1. If this is the legitimate same user, clear or update user.OidcId to the new oidcId (admin action) then re-login.
  2. If it's a different person, they must use a distinct email or a different account; do not merge blindly.
  3. Audit the existing user's OidcId/IdentityProvider to confirm which IdP owns the account.
  4. Ensure each user has a unique email across all linked IdPs to avoid future collisions.
Defensive patterns

Strategy: try-catch

Validate before calling

var existing = await unitOfWork.UserRepository.GetUserByEmailAsync(email, includes, ct);
if (existing != null && !string.IsNullOrEmpty(existing.OidcId) && existing.OidcId != oidcId)
    return Conflict("An account already exists for this email under a different identity.");

Try / catch

try { var user = await oidcService.LoginOrCreate(Request, principal, ct); }
catch (KavitaException ex) when (ex.Message == "errors.oidc.email-in-use")
{ return Conflict("Email already linked to another OIDC account."); }

Prevention

When it happens

Trigger: User A registered via OIDC with oidcId X and email E. Someone now logs in with a different oidcId Y but the same email E (e.g. E was reassigned, or a second IdP shares the address). GetUserByEmailAsync finds user A, sees OidcId X already set, and throws.

Common situations: User changed their email in the IdP; admin manually set an OidcId on a local account; two different IdP accounts share an email; leftover OidcId from a previous provider after migration.

Related errors


AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13). Data as JSON: /api/errors/0f3542201f3c7923. Report an issue: GitHub.