Kareadita/Kavita · error · KavitaException
errors.oidc.missing-external-id
Error message
errors.oidc.missing-external-id
What it means
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.
Source
Thrown at Kavita.Services/OidcService.cs:73
public const string IdToken = "id_token";
public const string ExpiresAt = "expires_at";
/// The name of the Auth Cookie set by .NET
public const string CookieName = ".AspNetCore.Cookies";
public static readonly List<string> DefaultScopes = ["openid", "profile", "offline_access", "roles", "email"];
private static readonly ConcurrentDictionary<string, bool> RefreshInProgress = new();
private static readonly ConcurrentDictionary<string, DateTimeOffset> LastFailedRefresh = new();
public async Task<AppUser?> LoginOrCreate(HttpRequest request, ClaimsPrincipal principal,
CancellationToken ct = default)
{
var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig;
var oidcId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(oidcId))
{
throw new KavitaException("errors.oidc.missing-external-id");
}
var user = await unitOfWork.UserRepository.GetByOidcId(oidcId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct);
if (user != null)
{
await SyncUserSettings(request, settings, principal, user);
return user;
}
var email = principal.FindFirstValue(ClaimTypes.Email);
if (string.IsNullOrEmpty(email))
{
throw new KavitaException("errors.oidc.missing-email");
}
if (settings.RequireVerifiedEmail && !principal.HasVerifiedEmail())
{View on GitHub (pinned to 9c3e540000)
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.
Defensive patterns
Strategy: validation
Validate before calling
var oidcId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(oidcId))
return Challenge("OIDC token is missing the subject (sub) claim."); Type guard
bool HasOidcSubject(ClaimsPrincipal p) =>
!string.IsNullOrWhiteSpace(p.FindFirstValue(ClaimTypes.NameIdentifier)); Try / catch
try { var user = await oidcService.LoginOrCreate(Request, principal, ct); }
catch (KavitaException ex) when (ex.Message == "errors.oidc.missing-external-id")
{ return Challenge(); // re-prompt OIDC with correct scopes } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- User is not authenticated
- errors.oidc.missing-email
- errors.oidc.no-account
- errors.oidc.email-not-verified
- errors.oidc.email-in-use
AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13).
Data as JSON: /api/errors/8a12ddf7e7bcf97d.
Report an issue: GitHub.