{"record":{"id":"0f3542201f3c7923","repo":"Kareadita/Kavita","slug":"errors-oidc-email-in-use","errorCode":null,"errorMessage":"errors.oidc.email-in-use","messagePattern":"errors\\.oidc\\.email-in-use","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/OidcService.cs","lineNumber":103,"sourceCode":"        if (string.IsNullOrEmpty(email))\n        {\n            throw new KavitaException(\"errors.oidc.missing-email\");\n        }\n\n        if (settings.RequireVerifiedEmail && !principal.HasVerifiedEmail())\n        {\n            throw new KavitaException(\"errors.oidc.email-not-verified\");\n        }\n\n\n        user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct);\n        if (user != null)\n        {\n            // Don't allow taking over accounts\n            // This could happen if the user changes their email in OIDC, and then someone else uses the old one\n            if (!string.IsNullOrEmpty(user.OidcId))\n            {\n                throw new KavitaException(\"errors.oidc.email-in-use\");\n            }\n\n            logger.LogDebug(\"User {UserName} has matched on email to {OidcId}\", user.Id, oidcId);\n            user.OidcId = oidcId;\n            await unitOfWork.CommitAsync(ct);\n\n            await SyncUserSettings(request, settings, principal, user);\n\n            return user;\n        }\n\n        return await CreateNewAccount(request, principal, settings, oidcId);\n    }\n\n    public async Task<AppUser?> RefreshCookieToken(CookieValidatePrincipalContext ctx, CancellationToken ct = default)\n    {\n        if (ctx.Principal == null) return null;\n","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/OidcService.cs#L85-L121","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["If this is the legitimate same user, clear or update user.OidcId to the new oidcId (admin action) then re-login.","If it's a different person, they must use a distinct email or a different account; do not merge blindly.","Audit the existing user's OidcId/IdentityProvider to confirm which IdP owns the account.","Ensure each user has a unique email across all linked IdPs to avoid future collisions."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"var existing = await unitOfWork.UserRepository.GetUserByEmailAsync(email, includes, ct);\nif (existing != null && !string.IsNullOrEmpty(existing.OidcId) && existing.OidcId != oidcId)\n    return Conflict(\"An account already exists for this email under a different identity.\");","typeGuard":null,"tryCatchPattern":"try { var user = await oidcService.LoginOrCreate(Request, principal, ct); }\ncatch (KavitaException ex) when (ex.Message == \"errors.oidc.email-in-use\")\n{ return Conflict(\"Email already linked to another OIDC account.\"); }","preventionTips":["Keep each user's email unique across linked IdPs.","When a user changes their IdP email, update their Kavita OidcId, don't create a new one.","Audit users with non-empty OidcId before bulk migrations."],"tags":["oidc","authentication","account-takeover","security"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}