Dynamic Authorization in .NET – A Practical Guide

Published on: April 9, 2025

Today, let’s dive into the world of dynamic authorization in .NET.

If you’ve ever built an app where permissions need to flex based on user roles, conditions, or even runtime data, you've likely bumped into this topic.

It's a powerful way to control access without hardcoding every rule into your app. I'll walk you through what it is, how to implement it, and share some real-world tips and gotchas I've stumbled across along the way.

☕️ Let’s get started!

What’s Dynamic Authorization Anyway?

In a nutshell, dynamic authorization means deciding "who can do what" at runtime, rather than baking it all into static code or config files.

Think of a scenario where a manager can approve expenses, but only if they're in the same department as the requester AND the amount is under $5,000.

That's too nuanced for a simple if (role == "Manager") check, right?

Dynamic authorization lets us evaluate these rules on the fly, using data from our app or even external systems.

In .NET, we've got a solid foundation with the built-in authorization framework (think Authorize attributes and policies), but we're going to push it further with custom logic.

Let's break it down into manageable steps and build something practical.

Step 1: Set Up the Basics with Policies

First things first, we need to lean on .NET's policy-based authorization.

Policies are like reusable rule sets that we can apply anywhere in our app. Let's say we're building a project management tool where users can edit tasks, but only under certain conditions.

In your Program.cs, configure a policy like this:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanEditTask", policy =>
        policy.Requirements.Add(new TaskEditRequirement()));
});

Here, TaskEditRequirement is a custom class we’ll create.

It's our hook for injecting dynamic logic. You'll also need to register a handler for this requirement. Don’t worry, we'll get there in a sec.

Gotcha: Don't forget to add the [Authorize] attribute to your controllers or endpoints.

For example:

[Authorize(Policy = "CanEditTask")]
[HttpPut("tasks/{taskId}")]
public IActionResult UpdateTask(int taskId, TaskModel model)
{
    // Task update logic here
    return Ok();
}

If you skip the policy name in the attribute, it'll just check for basic authentication (just if the user is authenticated) not what we want here.

Step 2: Define the Requirement

Next, let's create that TaskEditRequirement class. This is just a marker to tell .NET we've got a custom rule to enforce.

Keep it simple:

public class TaskEditRequirement : IAuthorizationRequirement
{
    // No properties needed yet, but we could add some later if the rule gets fancier
}

This class doesn't do much on its own, it's like a placeholder that says, "Hey, we've got a special condition to check.""

The real magic happens in the handler.

Step 3: Build the Authorization Handler

Here's where the dynamic part kicks in.

Create a handler to evaluate our rule. Let's say a user can edit a task if they're the task owner or a project admin.

We'll fetch the task data at runtime and decide.

public class TaskEditRequirementHandler : AuthorizationHandler<TaskEditRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly TaskService _taskService;

    public TaskEditRequirementHandler(IHttpContextAccessor httpContextAccessor, TaskService taskService)
    {
        _httpContextAccessor = httpContextAccessor;
        _taskService = taskService; // Assume this is your service to fetch task data
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                    TaskEditRequirement requirement)
    {
        // Get the task ID from the request
        var routeValues = _httpContextAccessor.HttpContext?.Request.RouteValues;

        if (!routeValues.TryGetValue("taskId", out var taskIdValue) ||
            !int.TryParse(taskIdValue?.ToString(), out int taskId))
        {
            return Task.CompletedTask; // No task ID? Can't authorize.
        }

        // Get the current user's ID (assuming it's in claims)
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (string.IsNullOrEmpty(userId))
        {
            return Task.CompletedTask; // No user? Can't authorize.
        }

        // Fetch the task (in a real app, this would/might be async and cached)
        var task = _taskService.GetTaskById(taskId);
        if (task == null)
        {
            return Task.CompletedTask; // Task doesn’t exist? Sorry.
        }

        // Dynamic logic: Can edit if owner or admin
        bool isOwner = task.OwnerId == userId;
        bool isAdmin = context.User.IsInRole("ProjectAdmin");

        if (isOwner || isAdmin)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Register it in the DI container in Program.cs:

builder.Services.AddScoped<IAuthorizationHandler, TaskEditRequirementHandler>();
builder.Services.AddHttpContextAccessor(); // <= Don’t forget this!

Lookup Tip: I’m using IHttpContextAccessor to grab the task ID from the route. It's super handy, but make sure your route names match what's in your controller. If your endpoint uses task_id instead of taskId, adjust accordingly.

Gotcha: If your service calls (like GetTaskById) throw exceptions, wrap them in a try-catch. A failure here shouldn't crash the app, it should just deny access gracefully.

Step 4: Add More Dynamic Conditions

Let's spice it up. What if only admins can edit tasks marked as "locked"?

Update the handler:

if (isOwner && !task.IsLocked) // Owners can edit unlocked tasks
{
    context.Succeed(requirement);
}
else if (isAdmin) // Admins can edit anything
{
    context.Succeed(requirement);
}

Now our logic adapts based on the task's state. This is where dynamic authorization shines: rules can shift with your data.

Gotcha: Watch out for performance. Fetching task data on every request could get slow. Consider caching (e.g., with IMemoryCache) if your app's scale demands it.

Step 5: Test It Out

Fire up your app and hit that endpoint with different users. Some scenarios to see it in action:

  • Log in as a regular user who owns a task: editing should work.
  • Lock the task and try again: access denied.
  • Switch to an admin account: editing works, locked or not.

If something's off, double-check your claims (are they in the token?) and route values (do they match your endpoint?).

Real-World Tips and Traps

  • Overcomplicating Policies: Keep policies focused. One per major feature (e.g., "CanEditTask" vs. a vague "CanEdit"). It's easier to maintain.
  • Async Pitfalls: My example's synchronous for simplicity, but GetTaskById should be async in practice. Update the handler to async Task and await it.
  • Claims Chaos: If your user's role or ID isn't in context.User, check your auth setup. JWT claims or cookie config might need tweaking.
  • Testing Edge Cases: Try a nonexistent task ID or a missing user. Your app should fail silently (return 403), not explode.

Wrapping Up

Dynamic authorization in .NET is a game-changer when you need flexible, data-driven access control.

By combining policies, requirements, and handlers, you can craft rules that adapt to your app's reality. No more rigid if-else nightmares.

Sure, there's a bit of setup, but once it's rolling, it's a breeze to extend.


Happy coding! ⚡


SecurityAuthorization