DOTNET Contents

Resource-Based Authorization

Resource-based authorization is how you prevent horizontal privilege escalation. Learn per-resource checks, tenant boundaries, and how to enforce ownership without leaking existence or creating N+1 auth calls.

On this page

Roles Are Not Enough for Real Systems

Most real authorization bugs are not “user is not admin”. They are: - user accesses another user's resource - tenant A reads tenant B data - support role can mutate resources it should only view This is horizontal privilege escalation, and it is the most common breach class in multi-tenant APIs. Production rule: Authorization must be tied to the resource, not just the identity.

Real Production Incident

Symptoms: - Customer reports seeing another customer's invoice. - Logs show authenticated requests with valid user tokens. - No admin role involved. Root cause: - Endpoint authorized by role only (User role). - Data query used invoice id without tenant/user scoping. - Any user who guessed or obtained another invoice id could access it. This is a classic missing resource-based authorization check.

Symptom → Cause → Diagnosis → Fix

Symptom: - cross-tenant or cross-user data exposure - “it only happens for certain IDs” - security incident with valid auth logs Cause: - authorization checks not scoped to resource - database queries missing tenant/owner filter - relying on client-provided userId/tenantId in request payload - inconsistent checks across endpoints Diagnosis: - Attempt access to a resource owned by another user in staging. - Audit queries: are they scoped by tenant_id and/or owner_id? - Review endpoints: do they check ownership or only roles? - Inspect logs for resource identifiers and user identifiers (carefully, no PII leaks). Fix: - Enforce resource-based authorization consistently. - Apply tenant/owner filters in data access layer. - Avoid separate “fetch then authorize” patterns that leak existence. - Ensure error semantics do not reveal sensitive existence information.

Anti-Pattern: Role-Only Authorization for Owned Resources

Example:
[Authorize(Policy = "User")]
[HttpGet("/v1/invoices/{id}")]
public async Task<IActionResult> Get(Guid id)
{
    var invoice = await db.Invoices.FindAsync(id);
    return Ok(invoice);
}
This says: “If you are a user, you can fetch any invoice by ID.” That is a breach.

Correct Pattern: Scope Data Access by Tenant/Owner

The simplest safe pattern is to scope the query:
var tenantId = User.FindFirst("tenant_id")!.Value;

var invoice = await db.Invoices
    .AsNoTracking()
    .Where(x => x.Id == id && x.TenantId == tenantId)
    .SingleOrDefaultAsync();
Now cross-tenant access fails naturally. Then decide response semantics: - If not found, return 404. - If you need explicit 403 vs 404, enforce policy intentionally. Production rule: Data access layer is part of authorization enforcement.

Resource-Based Authorization With Handlers

For complex rules, use resource-based authorization handlers. Conceptually: - load resource (or minimal resource projection) - authorize against user + resource - return allowed/denied But beware: If you load full resource first, you may leak data before authorization. Load only the minimal fields needed for auth decisions. Production rule: Authorize before returning sensitive fields.

Avoid N+1 Authorization Calls

If you authorize per item in a list endpoint, you can create: - N+1 database calls - latency spikes - throughput collapse under load Bad pattern: Fetch list, then authorize each item via separate query. Correct: Push authorization into query: - filter by tenant/owner - include only resources user can access - optionally apply role-based expansions (e.g., support can see more) Production rule: Authorization should be queryable, not loop-based.

Existence Leaks: 403 vs 404

Resource-based authorization interacts with information disclosure. If you return 403 for unauthorized resources, attackers can confirm existence. If you return 404, you hide existence but may confuse legitimate clients. Pick a policy: - For public identifiers: 403 may be fine. - For sensitive resources: 404 masking may be required. Production rule: Do not decide this ad-hoc per endpoint. Define a consistent rule.

Tenant Boundaries Are Non-Negotiable

In multi-tenant systems: - tenant scoping must be enforced everywhere - not “most places” - not “only for reads” Write operations must also be scoped: - updates must include tenant predicate - deletes must include tenant predicate - unique constraints may need tenant_id included Production rule: If tenant scoping is missing in one endpoint, your tenant isolation is broken.

Security Notes

- Never trust tenantId/userId from request payload for authorization. - Use claims from validated tokens. - Enforce scoping in persistence queries. - Consider defense in depth: database row-level security (where available), constraints, and strict service-layer checks.

Operational Notes

Monitoring: - Track authorization failures (403/404 masking) by endpoint. - Alert on unusual access patterns (many 404s for sequential IDs). - Monitor list endpoints for latency spikes due to authorization design. Rollout: - Tightening resource authorization can break clients relying on “too much access”. - Canary and measure access-denied rates. Rollback: - If authorization change blocks legitimate business flows, rollback quickly. - Do not weaken authorization permanently as a hotfix; fix the rule and re-ship. Runbooks: - Have an incident runbook for suspected cross-tenant leaks: - identify impacted tenant/user - audit access logs - rotate credentials if needed - patch and validate isolation

Checklist

- Owned/tenant resources are protected with resource-based authorization. - Data access queries include tenant/owner predicates. - No authorization decisions rely on client-provided userId/tenantId. - List endpoints avoid N+1 authorization patterns. - Existence leak policy (403 vs 404) is defined and consistent. - Authorization failures and suspicious access patterns are monitored. - Authorization tightening is canaried and rollbackable. - Runbook exists for cross-tenant exposure incidents.