fullstackhero/dotnet-starter-kit · error · UnauthorizedException

You can only reject top-up requests for your own tenant.

Error message

You can only reject top-up requests for your own tenant.

What it means

After loading the top-up request, non-root callers may only reject requests belonging to their own tenant. If request.TenantId differs from the caller's tenant Id, UnauthorizedException("You can only reject top-up requests for your own tenant.") is thrown (HTTP 403 semantics). Only the Root tenant can reject requests of any tenant.

Solutions

  1. Log in with credentials/tokens of the tenant that owns the top-up request, or have the Root tenant perform the rejection.
  2. Filter your UI's top-up request list by the caller's tenant so foreign Ids are never offered.
  3. Do not persist or share top-up request Ids across tenants; treat them as tenant-scoped resources.
  4. If legitimate cross-tenant rejection is required, perform it as the Root tenant.

Example fix

// before
// tenant user rejects arbitrary id
await mediator.Send(new RejectTopupRequestCommand(id));
// after
var req = await db.TopupRequests.FindAsync(id);
if (req == null || req.TenantId != callerTenantId) throw new UnauthorizedException("...own tenant.");
Defensive patterns

Strategy: validation

Validate before calling

var req = await db.TopupRequests.FindAsync(id);
if (req is null || req.TenantId != callerTenantId)
    return Forbid(); // or 404

Type guard

bool canReject(TopupRequest r, string callerTenantId, bool isRoot) => isRoot || r.TenantId == callerTenantId;

Try / catch

try { await mediator.Send(cmd); }
catch (UnauthorizedException ex) when (ex.Message.Contains("own tenant")) { return Forbid(); }

Prevention

When it happens

Trigger: A tenant-authenticated caller passes the Id of a top-up request that belongs to a different tenant (or the root tenant) — e.g. guessing/scraping GUIDs or a root-issued Id reused by a tenant user.

Common situations: Shared link to a top-up request between tenants; multi-tenant admin tooling logged in with the wrong tenant token; reusing test IDs across tenants.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/f24e7615dc964e47. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/RejectTopupRequest/RejectTopupRequestCommandHandler.cs:33

    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : ICommandHandler<RejectTopupRequestCommand, Guid>
{
    public async ValueTask<Guid> Handle(RejectTopupRequestCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");
        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;

        var request = await db.TopupRequests
            .FirstOrDefaultAsync(r => r.Id == command.Id, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Top-up request {command.Id} not found.");

        if (!isRoot && request.TenantId != callerTenantId)
        {
            throw new UnauthorizedException("You can only reject top-up requests for your own tenant.");
        }

        if (request.Status != TopupRequestStatus.Pending)
        {
            throw new CustomException(
                $"Top-up request {command.Id} cannot be rejected because it is {request.Status} (only Pending requests can be rejected).",
                (IEnumerable<string>?)null,
                HttpStatusCode.Conflict);
        }

        request.Reject(command.Reason);
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return request.Id;
    }
}

View on GitHub (pinned to 3f2959e683)