fullstackhero/dotnet-starter-kit · error · UnauthorizedException

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

Error message

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

What it means

This handler enforces tenant ownership manually (BillingDbContext is not tenant-filtered): if the caller is not root and the TopupRequest.TenantId differs from the caller's tenant id, it throws UnauthorizedException('You can only approve top-up requests for your own tenant.'). This prevents a tenant user from approving another tenant's top-up request and generating an invoice for it.

Solutions

  1. If cross-tenant approval is intended, authenticate as the root operator (root tenant context) and retry.
  2. Otherwise approve using credentials of the tenant that owns the request (send the request's own tenant id in __tenant__).
  3. Confirm the request's tenant: GET the top-up request and compare TenantId with your token's tenant.
  4. Fix client code that caches request ids across tenant logins/sessions.

Example fix

// before (tenant 'acme' approving request owned by 'globex')
await client.PostAsync($"/api/v1/wallets/topup-requests/{globexRequestId}/approve", null);
// after: root operator, or the owning tenant
client.DefaultRequestHeaders.Add("__tenant__", "globex");
await client.PostAsync($"/api/v1/wallets/topup-requests/{globexRequestId}/approve", null);
Defensive patterns

Strategy: validation

Validate before calling

var req = await api.GetTopupRequestAsync(id);
if (currentUserTenant != "root" && req.TenantId != currentUserTenant)
    throw new InvalidOperationException("Cannot approve another tenant's top-up request");

Type guard

bool CanApprove(string callerTenant, TopupRequestDto r) => callerTenant == "root" || r.TenantId == callerTenant;

Try / catch

try { await api.ApproveTopupRequestAsync(id); }
catch (UnauthorizedException ex) when (ex.Message.Contains("your own tenant"))
{
    // switch to root credentials or drop the operation
}

Prevention

When it happens

Trigger: A non-root tenant user approving (or double-approving after tenant reassignment) a request whose TenantId differs from the caller's resolved tenant — e.g. guessing/using another tenant's request id, or approving after the request was created under a different tenant identifier.

Common situations: Admins operating with a regular tenant token instead of root while trying to manage other tenants' wallets; multi-environment testing where the same request id is reused across tenants; a tenant user following a stale/deep link to another tenant's request.

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/3fe095ced60a966c. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs:33

    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : ICommandHandler<ApproveTopupRequestCommand, Guid>
{
    public async ValueTask<Guid> Handle(ApproveTopupRequestCommand 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 approve top-up requests for your own tenant.");
        }

        // For root, operate on the request's own tenant; for non-root, callerTenantId equals request.TenantId.
        var invoice = await billing.CreateTopupInvoiceAsync(request.TenantId, command.Id, cancellationToken)
            .ConfigureAwait(false);

        return invoice.Id;
    }
}

View on GitHub (pinned to 3f2959e683)