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
- If cross-tenant approval is intended, authenticate as the root operator (root tenant context) and retry.
- Otherwise approve using credentials of the tenant that owns the request (send the request's own tenant id in __tenant__).
- Confirm the request's tenant: GET the top-up request and compare TenantId with your token's tenant.
- 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
- Hide/Disable approve actions in the UI for requests whose TenantId differs from the current tenant.
- Use root credentials (root tenant context) for cross-tenant wallet administration.
- Clear cached request ids when switching tenant logins.
- Keep the ownership check in the handler even if DbContext filters are added later.
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
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
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)