Integrating scalable authorization in .NET

Published by Twain Taylor on July 22, 2025
Integrating scalable authorization in .NET

When developers start building enterprise applications with .NET Core Identity, we find that while authentication typically works seamlessly, authorization quickly becomes the bottleneck. Your go-to RBAC options will handle basic scenarios well enough, but as business requirements evolve and lean toward attribute-based decisions involving complex resource ownership, dynamic policies, and various other contextual factors, developers find themselves writing custom logic everywhere.

Traditional role and claims-based authorization forces impossible choices—either you maintain hundreds of granular roles, or you scatter business rules throughout your codebase. Both approaches create technical debt that compounds with every new requirement.

Cerbos solves this by externalizing authorization decisions to human-readable policies that business stakeholders can modify without touching code. This integration maintains your existing Identity authentication while enabling policy-driven access control that scales with organizational complexity.

This guide shows you how to integrate Cerbos with .NET Core Identity through custom authorization components. The result is maintainable, scalable access control that grows with your requirements.

Prerequisites

Before proceeding with the integration, ensure your development environment includes the .NET Core SDK (v6.0 or later) and Docker is installed for running Cerbos locally. We recommend Docker because it offers a straightforward approach for running Cerbos during development, though production deployments may use binary installations or container orchestration platforms.

This guide assumes familiarity with C# / ASP.NET Core fundamentals, including dependency injection, middleware, and Entity Framework Core basics.

Some experience with .NET Core Identity authentication flows will help, though we'll cover the integration points as we build them. If you're new to Identity, Microsoft's official documentation provides comprehensive setup guidance.

Familiarity with YAML syntax can also prove to be useful when authoring policies, though Cerbos policies remain intentionally straightforward and human-readable.

Basic .NET Core App with Identity

Creating the foundation requires establishing a new ASP.NET Core Web API project with Identity authentication. Start by creating a new project and installing the necessary NuGet packages for both Identity and Entity Framework Core integration. Here are the bash commands:

dotnet new webapi -n CerbosIdentityDemo
cd CerbosIdentityDemo
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Cerbos.Sdk

Next, configure Identity in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDefaultIdentity<IdentityUser>()
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddControllers();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

With that out of the way, let’s create the database context and define a resource model that would represent the entities that require protection:

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions options) : base(options) { }

    public DbSet<Document> Documents { get; set; }
}

public class Document
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string OwnerId { get; set; }
    public bool IsPublished { get; set; }
}

Now, let’s create a basic controller with standard CRUD operations:

[ApiController]
[Route("api/[controller]")]
public class DocumentsController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public DocumentsController(ApplicationDbContext context) => _context = context;

    [HttpGet("{id}")]
    public async Task<IActionResult> GetDocument(int id)
    {
        var document = await _context.Documents.FindAsync(id);
        return document == null ? NotFound() : Ok(document);
    }
}

Cerbos!

Cerbos cuts through the mess of custom instructions by treating authorization as what it actually is: a business decision that changes frequently and shouldn't live buried in your application code. So, instead of scattering conditional logic across controllers, you make a simple API call that returns "allow" or "deny" based on policies that business stakeholders can read and modify on their own volition. What’s more is that the platform operates as a stateless service, processing these decisions in microseconds, which means you get fine-tuned access control without the performance penalty that’s typically associated with it.

Getting Cerbos to run locally

Launch Cerbos using Docker and mount a local policies directory:

mkdir policies

docker run --rm --name cerbos -d \
  -v $(pwd)/policies:/policies \
  -p 3592:3592 -p 3593:3593 \
  ghcr.io/cerbos/cerbos:latest server

Writing your first policy

Cerbos policies follow a structured YAML format that defines rules for specific resource types and actions. Here’s a basic document policy in policies/document.yaml:

apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: "document"
  version: "default"
  rules:
    - actions: ["view"]
      effect: EFFECT_ALLOW
      roles: ["user"]
      condition:
        match:
          expr: "resource.attr.isPublished == true || resource.attr.ownerId == principal.id"

    - actions: ["edit", "delete"]
      effect: EFFECT_ALLOW
      roles: ["user"]
      condition:
        match:
          expr: "resource.attr.ownerId == principal.id"

    - actions: ["*"]
      effect: EFFECT_ALLOW
      roles: ["admin"]

Verify your policies compile without errors:

docker run --rm -v $(pwd)/policies:/policies \
  ghcr.io/cerbos/cerbos:latest compile /policies

Cerbos as a custom authorization provider in .NET Core

Time to bolt Cerbos onto the API and let it do the heavy lifting.

ASP.NET Core's authorization framework provides multiple extensibility points that enable integration with external authorization systems through custom policy providers and authorization handlers. With two small hooks, viz. IAuthorizationPolicyProvider and IAuthorizationHandler, you can swap the default role/claim checks for any decision engine you fancy. We’ll make those hooks point straight at the Cerbos PDP.

The requirement object:

public record CerbosRequirement(string Resource, string Action)
    : IAuthorizationRequirement;

A dynamic policy provider:

public class CerbosPolicyProvider : IAuthorizationPolicyProvider
{
    private readonly DefaultAuthorizationPolicyProvider _fallback;

    public CerbosPolicyProvider(IOptions<AuthorizationOptions> opts) =>
        _fallback = new DefaultAuthorizationPolicyProvider(opts);

    public Task<AuthorizationPolicy> GetPolicyAsync(string name)
    {
        if (!name.StartsWith("Cerbos:", StringComparison.OrdinalIgnoreCase))
            return _fallback.GetPolicyAsync(name);

        // Expected format: Cerbos:resource:action
        var parts = name.Split(':', 3);
        var policy = new AuthorizationPolicyBuilder()
            .AddRequirements(new CerbosRequirement(parts[1], parts[2]))
            .Build();

        return Task.FromResult(policy);
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>
        _fallback.GetDefaultPolicyAsync();

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() =>
        _fallback.GetFallbackPolicyAsync();
}

The Cerbos authorization handler:

public class CerbosAuthorizationHandler
    : AuthorizationHandler<CerbosRequirement, Document>
{
    private readonly ICerbosClient _cerbos;

    public CerbosAuthorizationHandler(ICerbosClient cerbos) => _cerbos = cerbos;

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext ctx,
        CerbosRequirement req,
        Document doc)
    {
        if (!ctx.User.Identity?.IsAuthenticated ?? true) return;

        var roles = ctx.User.FindAll(ClaimTypes.Role).Select(r => r.Value).ToArray();
        var principal = Principal.NewInstance(ctx.User.Identity!.Name!, roles);

        var entry = ResourceEntry.NewInstance(req.Resource, doc.Id.ToString())
            .WithAttribute("ownerId", AttributeValue.StringValue(doc.OwnerId))
            .WithAttribute("isPublished", AttributeValue.BoolValue(doc.IsPublished))
            .WithActions(req.Action);

        var allowed = (await _cerbos.CheckResourcesAsync(
                CheckResourcesRequest.NewInstance()
                    .WithPrincipal(principal)
                    .WithResourceEntries(entry)))
            .Find(doc.Id.ToString())
            .IsAllowed(req.Action);

        if (allowed) ctx.Succeed(req);
    }
}

Wire it up and call the pipeline:

builder.Services.AddSingleton(_ =>
    CerbosClientBuilder.ForTarget("http://localhost:3593")
        .WithPlaintext()
        .Build());

builder.Services.AddSingleton<IAuthorizationPolicyProvider, CerbosPolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, CerbosAuthorizationHandler>();

[HttpGet("{id}")]
public async Task<IActionResult> GetDocument(
    int id,
    [FromServices] IAuthorizationService auth)
{
    var doc = await _db.Documents.FindAsync(id);
    if (doc is null) return NotFound();

    var decision = await auth.AuthorizeAsync(User, doc, "Cerbos:document:view");
    return decision.Succeeded ? Ok(doc) : Forbid();
}

Hands-on testing

Testing your Cerbos integration would unravel the tangible benefits of policy-driven authorization. For starters, log in as a regular user and access a document you own. The system should permit this request because your policy explicitly allows owners to view their resources. Next attempt to access another user's document using the same account. Cerbos should block this request with a 403 Forbidden response since the policy restricts access to non-owners.

Next, switch to an administrative account to verify elevated privileges. The admin should access any document regardless of ownership because your policy grants wildcard access to administrative roles. But the real validation shall be performed when you actually modify the policies. To that end, adjust your YAML file to change editing permissions from document owners to managers only. If everything has been implemented correctly, the updated rules should immediately enforce the new policy when you retest the endpoints without the need to bounce the application.

Parting thoughts

By pairing Cerbos with .NET Core Identity, you move past rigid role checks and finally get authorization that’s both dynamic and maintainable. Now your policies live where they belong—outside your app, ready to change as your needs do. Dive deeper into Cerbos, experiment with richer policies, and join a growing community that’s rethinking access control for the real world.

If you want to dive deeper into implementing and managing authorization, join one of our engineering demos or check out our in-depth documentation.

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team