{"record":{"id":"8a12ddf7e7bcf97d","repo":"Kareadita/Kavita","slug":"errors-oidc-missing-external-id","errorCode":null,"errorMessage":"errors.oidc.missing-external-id","messagePattern":"errors\\.oidc\\.missing-external-id","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/OidcService.cs","lineNumber":73,"sourceCode":"    public const string IdToken = \"id_token\";\n    public const string ExpiresAt = \"expires_at\";\n\n    /// The name of the Auth Cookie set by .NET\n    public const string CookieName = \".AspNetCore.Cookies\";\n    public static readonly List<string> DefaultScopes = [\"openid\", \"profile\", \"offline_access\", \"roles\", \"email\"];\n\n    private static readonly ConcurrentDictionary<string, bool> RefreshInProgress = new();\n    private static readonly ConcurrentDictionary<string, DateTimeOffset> LastFailedRefresh = new();\n\n    public async Task<AppUser?> LoginOrCreate(HttpRequest request, ClaimsPrincipal principal,\n        CancellationToken ct = default)\n    {\n        var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig;\n\n        var oidcId = principal.FindFirstValue(ClaimTypes.NameIdentifier);\n        if (string.IsNullOrEmpty(oidcId))\n        {\n            throw new KavitaException(\"errors.oidc.missing-external-id\");\n        }\n\n        var user = await unitOfWork.UserRepository.GetByOidcId(oidcId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct);\n        if (user != null)\n        {\n            await SyncUserSettings(request, settings, principal, user);\n\n            return user;\n        }\n\n        var email = principal.FindFirstValue(ClaimTypes.Email);\n        if (string.IsNullOrEmpty(email))\n        {\n            throw new KavitaException(\"errors.oidc.missing-email\");\n        }\n\n        if (settings.RequireVerifiedEmail && !principal.HasVerifiedEmail())\n        {","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/OidcService.cs#L55-L91","documentation":"Thrown in OidcService.LoginOrCreate when the OIDC principal has no ClaimTypes.NameIdentifier (the 'sub' claim). Without a stable external identifier Kavita cannot map or create the user, so login is aborted early before any DB lookup. This is the first validation gate in the OIDC login flow.","triggerScenarios":"An OIDC callback where the IdToken/access token lacks the 'sub' claim, or the claim isn't mapped to ClaimTypes.NameIdentifier. Common with misconfigured scopes/claim mappings or a provider that names the identifier differently.","commonSituations":"The 'openid' scope wasn't requested (no sub claim); provider issues the identifier under a custom claim (e.g. 'uid') without a NameIdentifier mapping; token validation pipeline stripped the claim; clock/sig failure caused a partial principal.","solutions":["Ensure the OIDC scopes include 'openid' so the provider emits a 'sub' claim (Kavita's DefaultScopes include it).","Configure the provider/claim-mapping to emit the user identifier as ClaimTypes.NameIdentifier, or remap the custom claim.","Inspect the decoded IdToken (jwt.io) to confirm 'sub' is present and populated.","Verify the OIDC authority/metadata endpoint is correct so claims are validated and attached."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"var oidcId = principal.FindFirstValue(ClaimTypes.NameIdentifier);\nif (string.IsNullOrEmpty(oidcId))\n    return Challenge(\"OIDC token is missing the subject (sub) claim.\");","typeGuard":"bool HasOidcSubject(ClaimsPrincipal p) =>\n    !string.IsNullOrWhiteSpace(p.FindFirstValue(ClaimTypes.NameIdentifier));","tryCatchPattern":"try { var user = await oidcService.LoginOrCreate(Request, principal, ct); }\ncatch (KavitaException ex) when (ex.Message == \"errors.oidc.missing-external-id\")\n{ return Challenge(); // re-prompt OIDC with correct scopes }","preventionTips":["Always request the 'openid' scope so 'sub' is emitted.","Map any custom identifier claim to ClaimTypes.NameIdentifier.","Decode the IdToken during setup to confirm 'sub' is present."],"tags":["oidc","authentication","claims","login"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}