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
- Log in with credentials/tokens of the tenant that owns the top-up request, or have the Root tenant perform the rejection.
- Filter your UI's top-up request list by the caller's tenant so foreign Ids are never offered.
- Do not persist or share top-up request Ids across tenants; treat them as tenant-scoped resources.
- 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
- Only surface top-up request Ids that belong to the caller's tenant in the UI.
- Don't share top-up request links across tenants.
- Use Root credentials for cross-tenant administration.
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
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
- 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/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)