Kareadita/Kavita · error · KavitaException

not-authenticated

Error message

not-authenticated

What it means

Thrown by StreamService.CreateExternalSource when GetUserByIdAsync(userId, AppUserIncludes.ExternalSources) returns null. Despite the key 'not-authenticated', the request was authenticated — the real condition is that the principal's UserId claim does not resolve to an AppUser row (deleted account, stale/invalid token, corrupted claim). Unlike the surrounding throws this one uses a hard-coded string rather than localizationService.TranslateAsync. As a plain KavitaException it surfaces as HTTP 500.

Source

Thrown at Kavita.Services/StreamService.cs:290

            wantedPosition = list.IndexOf(itemAtWantedPosition);
        }

        OrderableHelper.ReorderItems(list, stream.Id, wantedPosition);
        user.SideNavStreams = list;

        unitOfWork.UserRepository.Update(user);
        await unitOfWork.CommitAsync(ct);
        if (!stream.Visible) return;
        await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
            userId, ct);
    }

    public async Task<ExternalSourceDto> CreateExternalSource(int userId, ExternalSourceDto dto,
        CancellationToken ct = default)
    {
        var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId,
            AppUserIncludes.ExternalSources, ct);
        if (user == null) throw new KavitaException("not-authenticated");

        if (user.ExternalSources.Any(s => s.Host == dto.Host))
        {
            throw new KavitaException("external-source-already-exists");
        }

        if (string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required");
        if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format");


        var newSource = new AppUserExternalSource()
        {
            Name = dto.Name,
            Host = UrlHelper.EnsureEndsWithSlash(UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)),
            ApiKey = dto.ApiKey
        };
        user.ExternalSources.Add(newSource);

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Have the client log out and re-authenticate to obtain a fresh token, then retry.
  2. On the server, verify the UserId claim maps to a live user at the auth boundary; if not, return 401 instead of reaching the service.
  3. Ensure user-merge/delete flows revoke outstanding tokens so stale ids cannot reach this code.

Example fix

// before
public async Task<ActionResult<ExternalSourceDto>> CreateExternalSource(ExternalSourceDto dto)
    => Ok(await streamService.CreateExternalSource(UserId, dto));
// after
public async Task<ActionResult<ExternalSourceDto>> CreateExternalSource(ExternalSourceDto dto)
{
    if (UserId <= 0) return Unauthorized();
    return Ok(await streamService.CreateExternalSource(UserId, dto));
}
Defensive patterns

Strategy: validation

Validate before calling

function hasValidUserId(userId: number | null | undefined): boolean {
  return typeof userId === 'number' && userId > 0;
}

Try / catch

// on 'not-authenticated' from create-external-source: clear local session and re-authenticate
this.auth.logout(); this.router.navigate(['/login']);

Prevention

When it happens

Trigger: POST /api/streams/create-external-source issued with a valid JWT whose UserId claim points to a user that no longer exists in the database (admin deleted the account mid-session, or the claim is stale).

Common situations: Account was deleted or the user was migrated to a new id while the old token was still accepted; a test/migration inserted an orphaned JWT; UserId resolution in BaseApiController returned a value not present in the users table.

Understand the failure class

Related errors


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