Advanced 55 min.NET 10

Authorization that protects the data

Go beyond a login gate with role policies, owner-scoped queries, and boundary tests.

BY THE END OF THIS TUTORIAL

Here's what you'll be able to do.

  • Distinguish roles, policies, and ownership
  • Enforce authorization on every read and write
  • Avoid insecure direct object references
Before you begin

Complete Identity. Use two local test accounts and inspect TasksController.

Understand the idea

A signed-in user is not automatically allowed to access every record. The dangerous gap often appears when an action accepts an id, loads that row, and assumes the user reached it through a legitimate link. Anyone can alter a URL or form value.

TaskBoard’s resource rule is simple: a user may access only their own tasks. That rule must hold for list, edit, toggle, delete, and API endpoints. Hiding a button is a presentation choice; it is never the enforcement point.

Choose your environment

Open TaskBoard.csproj from the example download. Install the ASP.NET and web development workload and a Visual Studio release supporting .NET 10. Select the TaskBoard launch profile and press F5. Use Solution Explorer to find the files named below.

Download the complete companion project
STEP 01

Express broad access rules

[Authorize] requires an authenticated user. A role requirement can gate an administrative operation. A named policy gives the rule a stable name so multiple endpoints can share it. TaskBoard registers ManageBoard as an example but does not grant the role to new users.

Role assignment should be an administrative process. Never accept an IsAdmin checkbox during public registration. For richer requirements, authorization handlers can evaluate claims and resources without scattering the same conditions throughout controllers.

Program.cs excerpt
builder.Services.AddAuthorization(options =>
    options.AddPolicy("ManageBoard",
        policy => policy.RequireRole("Admin")));

// On a future administrative action:
// [Authorize(Policy = "ManageBoard")]
STEP 02

Scope the query to the current owner

Filter by both id and owner before retrieving a private record. Returning 404 for a missing or unowned row prevents the endpoint from disclosing whether another user’s id exists. The list and API queries use the same owner boundary.

The current owner id comes from the authenticated principal. A posted OwnerId field would be untrusted input. Even if the view never renders such a field, a client can submit one manually.

Controller excerpt
var task = await db.Tasks.SingleOrDefaultAsync(
    t => t.Id == id && t.OwnerId == OwnerId, ct);
if (task is null) return NotFound();
STEP 03

Repeat the check on mutations

A user can open an edit page, wait, and then submit after permissions change. They can also skip the GET entirely. Therefore the POST must load and authorize the row again. Antiforgery does not substitute for this check; it addresses a different threat.

For more complex resource rules, load the resource and call IAuthorizationService.AuthorizeAsync with a resource-specific requirement. Keep the final authorization decision next to the mutation so the relationship remains obvious in review.

Controller excerpt
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Toggle(int id, CancellationToken ct)
{
    var task = await db.Tasks.SingleOrDefaultAsync(
        t => t.Id == id && t.OwnerId == OwnerId, ct);
    if (task is null) return NotFound();
    task.IsComplete = !task.IsComplete;
    await db.SaveChangesAsync(ct);
    return RedirectToAction(nameof(Index));
}
STEP 04

Prove the boundary with two identities

Use account A to create a task and note its id. Sign in as B and attempt to read and edit that id. The operation should fail, and A’s row should remain unchanged. Also test an unauthenticated client: API authentication failures and ownership failures are different cases.

The companion tests exercise unauthorized API requests and owned versus unowned records. A test that merely checks whether the controller has an attribute cannot prove the query enforces ownership.

Boundary matrix
Anonymous + private task → authentication challenge
Owner + existing task    → allowed
Different user + task    → 404, no mutation
Owner + missing task     → 404
Missing antiforgery token + form POST → 400
YOUR TURN

Make it your own.

Try to edit one account’s task while signed in as a second account. Verify both the response and the unchanged database state.

See the solution & reasoning

The filtered query returns no row, so the action returns NotFound before applying changes. Verifying only a 404 is weaker than also checking the original task title remains intact. A regression test should assert both behavior and state.

When something doesn't work

Do not confuse 401 with 403. An unauthenticated request needs authentication; an authenticated user may still lack a required permission. Browser cookie handlers may redirect to appropriate UI pages.

Role changes may require cookie refresh or security-stamp handling before existing sessions see them.

An antiforgery token can be valid for a request that still targets someone else’s record. Always authorize the resource.

CHECK YOUR UNDERSTANDING

Where must task ownership be checked?

Where must task ownership be checked?

ONE STEP FURTHER

That's another skill in your toolkit.

Your progress is saved in this browser. Sign in to save completion to your account.