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

  1. Ensure the OIDC scopes include 'openid' so the provider emits a 'sub' claim (Kavita's DefaultScopes include it).
  2. Configure the provider/claim-mapping to emit the user identifier as ClaimTypes.NameIdentifier, or remap the custom claim.
  3. Inspect the decoded IdToken (jwt.io) to confirm 'sub' is present and populated.
  4. 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

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


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