ASP.NET Core / Intermediate: Data, Identity, and Asynchronous Work / Lesson 10
Authorization and Resource Boundaries
Apply policy and object-level checks using trusted identity and current resource state, not just a successful login.
On this page
Lesson goals
- Explain policies, requirements, handlers, and fallback rules.
- Check ownership and tenant scope before effects.
- Test policy components without claiming authentication integration.
A policy expresses required authority
Authorization evaluates whether a principal may perform an operation on a resource. A policy can require an authenticated identity, claims/roles, or custom requirements. Requirements in one policy normally all need satisfaction, while several handlers can support one requirement according to the framework’s rules. Explicit failure and handler execution options deserve review rather than assumptions about short-circuiting.
Named, default, and fallback policies have different selection roles. Fallback can protect endpoints lacking authorization metadata when authorization middleware runs, but it does not magically protect static content served earlier. AllowAnonymous is an explicit exception that needs review. Keep the intended public surface documented and tested.
Evaluate a named policy with synthetic claims
The first complete program uses the real authorization service with a local policy and invented principals. It confirms that the reader fixture satisfies authenticated-plus-scope requirements and the empty fixture does not. It neither validates credentials nor turns the result into an HTTP challenge/forbid response. Those are separate integration behaviors.
A scope/role string must come from a trusted identity path. Do not grant authority because a route, query, or custom header contains a matching word. Claim-type and value mapping can affect policy outcomes, so keep issuer/handler configuration aligned with the policy and test both positive and negative cases.
using System;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging();
services.AddAuthorization(options => options.AddPolicy("ReadReports", policy => policy.RequireAuthenticatedUser().RequireClaim("scope", "reports.read")));
using var provider = services.BuildServiceProvider();
var authorization = provider.GetRequiredService<IAuthorizationService>();
var reader = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("scope", "reports.read") }, "synthetic"));
var empty = new ClaimsPrincipal(new ClaimsIdentity());
bool allowed = (await authorization.AuthorizeAsync(reader, null, "ReadReports")).Succeeded;
bool denied = (await authorization.AuthorizeAsync(empty, null, "ReadReports")).Succeeded;
Console.WriteLine($"reader={allowed} empty={denied}");
reader=True empty=False
Only the synthetic reader satisfies both requirements; no token or hosted middleware is involved.
Authentication does not establish ownership
A route ID identifies a requested resource, not the right to access it. Load only under a bounded authorized data-access design, then enforce operation-specific owner/tenant/state rules before exposing or mutating data. A broad reports.read claim may not grant every report. Do not trust a tenant identifier solely because the client supplied it.
Resource-based authorization can use IAuthorizationService with the actual resource and custom handlers. Keep policy and domain invariants consistent with current persisted state. If state can change between checking and updating, concurrency/transaction design may be needed; an early Boolean check is not a universal atomic permission-to-write guarantee.
Test a resource handler with an explicit resource
The second program directly executes a handler that checks both owner and tenant. It deliberately compares a permitted and a wrong-tenant case. This verifies one handler’s rule, not the entire policy graph, database isolation, or endpoint middleware. A complete application must decide its forbidden/not-found disclosure policy and test it consistently.
Keep denial messages and logs useful without leaking another tenant’s data. Audit policy changes and anonymous exceptions. A test that only checks an administrator path can miss the ordinary user or cross-tenant case where the real boundary is violated.
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "u1"), new Claim("tenant", "t1") }, "synthetic"));
var requirement = new OwnerRequirement();
var allowed = new AuthorizationHandlerContext(new[] { requirement }, user, new Report("u1", "t1"));
var wrongTenant = new AuthorizationHandlerContext(new[] { requirement }, user, new Report("u1", "t2"));
var handler = new OwnerHandler();
await handler.HandleAsync(allowed);
await handler.HandleAsync(wrongTenant);
Console.WriteLine($"owner={allowed.HasSucceeded} wrongTenant={wrongTenant.HasSucceeded}");
internal sealed class OwnerRequirement : IAuthorizationRequirement { }
internal sealed record Report(string Owner, string Tenant);
internal sealed class OwnerHandler : AuthorizationHandler<OwnerRequirement, Report>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnerRequirement requirement, Report resource)
{
if (context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value == resource.Owner && context.User.FindFirst("tenant")?.Value == resource.Tenant)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
owner=True wrongTenant=False
The synthetic same-owner/same-tenant resource passes; the cross-tenant resource does not.
Verify the policy at the public operation boundary
Test anonymous, authenticated-but-missing-authority, ordinary allowed, owner mismatch, tenant mismatch, and changed-resource cases. Include policy selection and middleware order in isolated integration coverage. A direct handler test does not prove that the endpoint requires the policy, that a fallback is selected, or that challenge/forbid is mapped correctly.
Keep authorization close enough to the effect that its assumptions remain valid. Do not cache a sensitive decision without a clear identity/resource/version lifetime. Reusable services should not silently bypass checks because a controller was expected to perform them; define the application’s trusted entry points and enforce their contract.
Common errors and fixes
A route parameter is trusted as tenant authority.
- Why it happens
- Client input replaces a trusted identity/resource relationship.
- How to fix it
- Derive tenant scope from a validated authorization design.
- Prevent it next time
- Test cross-tenant access explicitly.
A passing handler test is called endpoint protection.
- Why it happens
- The endpoint/policy/middleware binding was not exercised.
- How to fix it
- Verify the public policy requirement in integration.
- Prevent it next time
- Test anonymous and missing-policy paths.
An old cached decision authorizes changed resource state.
- Why it happens
- Decision lifetime exceeds its valid assumptions.
- How to fix it
- Use bounded/version-aware caching or reevaluate before effects.
- Prevent it next time
- Review identity and resource invalidation.
Check your understanding
1. A user has reports.read and asks for another tenant’s report. Why may a claim-only policy be insufficient?
Show worked solution
The claim can grant a category of operation without identifying which tenant’s resources are allowed. Enforce trusted tenant/owner scope against the actual resource and data-access boundary. Test the cross-tenant case without leaking the report. A generic read claim is not a universal grant to every object, and the HTTP status/disclosure policy must be consistent.
2. Why does authorization sometimes need to be considered together with optimistic concurrency?
Show worked solution
A resource’s owner or workflow state may change after a check but before an update. A version/concurrency token or transaction design can detect/restrict stale writes under the provider’s semantics. Authorization still establishes permitted intent, while concurrency protects the state assumption. Test both and do not claim that one early policy result makes a later database operation atomic.
Key takeaways
- A trusted identity is necessary but not sufficient for resource access.
- Policy selection and resource rules both require negative tests.
- Decision lifetime must match the state and effect it authorizes.