End-to-end coding rules for building, testing, and operating Role-Based Access Control in ASP.NET Core 8+ back-ends secured by Microsoft Identity.Web (Azure AD) or alternative IdPs.
You've been there: implementing role checks scattered across controllers, hardcoding permissions in business logic, and debugging authorization failures with zero visibility into what went wrong. Meanwhile, your security team is asking for audit trails, compliance reports, and the ability to quickly revoke access when someone changes teams.
Most .NET applications start with simple [Authorize(Roles = "Admin")] attributes and evolve into authorization nightmares:
When your startup becomes an enterprise, or when compliance auditors come knocking, these ad-hoc approaches crumble under scrutiny.
These Cursor Rules transform your authorization approach from scattered checks to a centralized, auditable system built on ASP.NET Core's authorization framework with Microsoft Identity.Web integration.
Instead of this authorization chaos:
// Scattered across your codebase
public class UsersController : ControllerBase
{
public async Task<IActionResult> GetUsers()
{
if (!User.IsInRole("Admin") && !User.IsInRole("Manager"))
return Forbid();
// business logic...
}
}
public class ReportsController : ControllerBase
{
public async Task<IActionResult> GetFinancialReport()
{
// Different developer, different approach
if (!User.HasClaim("permission", "read-financial"))
return Unauthorized();
// business logic...
}
}
You get this structured approach:
// Centralized authorization definitions
public static class Roles
{
public const string Admin = "Admin";
public const string Manager = "Manager";
public const string Analyst = "Analyst";
}
public static class Policies
{
public const string CanManageUsers = "CanManageUsers";
public const string CanViewFinancials = "CanViewFinancials";
public const string CanManageReports = "CanManageReports";
}
// Clean controller implementation
[Authorize(Policy = Policies.CanManageUsers)]
public class UsersController : ControllerBase
{
public async Task<IActionResult> GetUsers()
{
// Pure business logic - authorization handled declaratively
return Ok(await _userService.GetUsersAsync());
}
}
Faster Feature Development: No more hunting through legacy code to understand authorization patterns. New developers can implement secure endpoints in minutes using established policies.
Zero-Security-Debt Deployments: Every endpoint is secure by design. The policy-driven approach prevents unauthorized access from reaching production.
Audit-Ready Architecture: Built-in structured logging captures every authorization decision. Compliance reports become automated queries instead of manual investigations.
Maintainable Permission Changes: Role modifications happen in one place and propagate automatically. No more "did we update all 23 controllers?" deployment anxiety.
Before:
After:
// 1. Define policy once (if new)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(Policies.CanManageInventory, policy =>
policy.RequireRole(Roles.Manager, Roles.Admin));
});
// 2. Apply to endpoint
[Authorize(Policy = Policies.CanManageInventory)]
public async Task<IActionResult> UpdateInventory(InventoryRequest request)
{
// Pure business logic
}
Time investment: 5 minutes per endpoint
Before:
After:
// Automatic structured logging
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "Warning",
"message": "Authorization failed",
"userId": "user123",
"policy": "CanManageUsers",
"requirement": "RequireRole: Admin,Manager",
"userRoles": ["Analyst"],
"traceId": "abc123"
}
Resolution time: 2-3 minutes with log queries
Before:
After:
// Single source of truth
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(Policies.CanManageUsers, policy =>
policy.RequireRole(Roles.Admin, Roles.Manager, Roles.HRLead)); // Added HRLead
});
Change cycle: 20 minutes including automated tests
Create your authorization foundation:
src/
├── Authorization/
│ ├── Constants.cs // Centralized role and policy definitions
│ ├── Policies/ // Custom policy definitions
│ ├── Handlers/ // Complex authorization logic
│ └── Requirements/ // Custom authorization requirements
├── docs/
│ └── rbac/
│ └── README.md // Auto-generated policy documentation
└── roles.json // Declarative role matrix (version controlled)
// Program.cs
builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(Policies.CanManageUsers, policy =>
policy.RequireRole(Roles.Admin, Roles.Manager));
options.AddPolicy(Policies.CanViewFinancials, policy =>
policy.RequireRole(Roles.Admin, Roles.FinanceManager));
options.AddPolicy(Policies.CanManageReports, policy =>
policy.RequireAssertion(context =>
context.User.IsInRole(Roles.Admin) ||
(context.User.IsInRole(Roles.Manager) &&
context.User.HasClaim("department", "analytics"))));
});
// Custom authorization failure handling
builder.Services.AddScoped<IAuthorizationMiddlewareResultHandler, CustomAuthorizationMiddlewareResultHandler>();
public class AuthorizationLoggingHandler : IAuthorizationHandler
{
private readonly ILogger<AuthorizationLoggingHandler> _logger;
public Task HandleAsync(AuthorizationHandlerContext context)
{
if (!context.HasSucceeded)
{
_logger.LogWarning("Authorization failed for user {UserId} on policy {Policy}. Requirements: {Requirements}",
context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
context.Resource?.ToString(),
string.Join(", ", context.Requirements.Select(r => r.GetType().Name)));
}
return Task.CompletedTask;
}
}
// Integration test setup
public class AuthorizationTests : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task GetUsers_WithAdminRole_ReturnsOk()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
builder.ConfigureTestServices(services =>
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthenticationSchemeHandler>("Test", options => { })));
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test", CreateJwtToken(Roles.Admin));
// Act
var response = await client.GetAsync("/api/users");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task GetUsers_WithAnalystRole_ReturnsForbidden()
{
// Test negative cases for every protected endpoint
}
}
Your authorization system shouldn't be a source of technical debt and security anxiety. These Cursor Rules give you the foundation to build RBAC that scales with your application and satisfies both your development team and security auditors.
The difference between ad-hoc role checks and structured authorization policies is the difference between hoping your security works and knowing it works. Choose the approach that lets you ship features confidently and sleep soundly.
You are an expert in C#, ASP.NET Core 8, Microsoft Identity.Web, Azure AD (Microsoft Entra ID), Okta, SailPoint, Casbin, Cerbos, Kubernetes RBAC.
Key Principles
- Start with a written RBAC matrix that maps Roles → Resources → Actions. Keep it under version control (policy-as-code).
- Apply the Principle of Least Privilege (PoLP) everywhere: APIs, queues, storage, data layer.
- Centralize authorization logic; never hard-code role checks in random services.
- Prefer declarative policies (attributes, JSON, YAML) over imperative scattered checks.
- All authorization decisions must be auditable; emit structured logs for every allow/deny.
- Treat roles as part of the domain model; changes follow the same review & CI process as code.
- Avoid role explosion; use composite roles or claims-based policies when >20 roles appear.
C# / .NET 8
- Organize auth code in an Authorization folder: ⟶ Policies/, Handlers/, Requirements/, Constants.cs.
- Keep role & policy names as public const string (PascalCase) to prevent typos:
```csharp
public static class Roles { public const string Admin = "Admin"; public const string Manager = "Manager"; }
public static class Policies { public const string CanManageUsers = "CanManageUsers"; }
```
- Always use async Task methods inside AuthorizationHandlers; call `context.Succeed` or `context.Fail` once.
- Prefer named policies over the `Roles` property when the rule is anything more than “is X”.
- NEVER derive custom claims from partial client input. Only trust IdP-issued claims.
- Place `[Authorize]` attributes at the controller or minimal-API endpoint level—not inside business methods.
- For constant lists (e.g., allowed roles) use `HashSet<string>` initialized in a static readonly field for O(1) lookups.
- Use dependency injection to supply data-access services to handlers; avoid `new DbContext()` in handlers.
Error Handling & Validation
- Return 401 for unauthenticated, 403 for authenticated but unauthorized.
- Wrap authorization failures in ProblemDetails JSON with trace-id.
- Emit `AuthorizationFailedEvent` + denied principal + requirement in structured logs.
- Log high-value role grants & removals separately (audit trails).
- Early-return pattern in handlers:
```csharp
if (!user.IsInRole(Roles.Admin)) { context.Fail(); return Task.CompletedTask; }
```
- Fallback handler: register a catch-all `IAuthorizationMiddlewareResultHandler` to mask sensitive details from clients.
ASP.NET Core Authorization Rules
- Register authentication & authorization in Program.cs:
```csharp
builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(Policies.CanManageUsers, policy =>
policy.RequireRole(Roles.Admin, Roles.Manager));
});
```
- In minimal APIs:
```csharp
app.MapPost("/users", CreateUser)
.RequireAuthorization(Policies.CanManageUsers);
```
- Use `IAuthorizationService.AuthorizeAsync` for imperative checks, e.g., inside SignalR hubs.
- Custom requirement sample:
```csharp
public class SameTenantRequirement : IAuthorizationRequirement {}
public class SameTenantHandler : AuthorizationHandler<SameTenantRequirement>
{
protected override Task HandleRequirementAsync(...) { /* validate tenantId */ }
}
```
- Wire external engines (Casbin/Cerbos) via a single adapter service implementing `IAuthorizationPolicyProvider` to allow hot-reload of policies.
Identity Provider Integration
- Azure AD: add `"groups": {"securityGroup": true}` in `appsettings.json` to map group IDs to role claims.
- Okta: configure custom claim `preferred_username` and role groups in the Authorization Server.
- For hybrid scenarios use SailPoint or SCIM for automated role provisioning.
Testing
- Create fake JWTs with required role claims using `JwtSecurityTokenHandler` in unit tests.
- For integration tests use `WebApplicationFactory` + `WithWebHostBuilder` overriding authentication to inject test identities.
- Include negative tests (expect 403) for every protected endpoint.
- Add contract tests ensuring that newly introduced roles don’t gain unintended access (regression guard).
Performance
- Cache IdP introspection (groups → role) for 5 minutes using IMemoryCache.
- For high-QPS public APIs, extract roles into JWTs to avoid per-request graph calls.
- Use policy evaluation benchmarking (`dotnet-counters`, `BenchmarkDotNet`) before deploying new handlers.
Security & Compliance
- Enforce MFA on all role-management portals.
- Implement Just-In-Time (JIT) elevation: temporary Admin role via PIM (Privileged Identity Management).
- Rotate signing keys every 30 days; implement automatic key rollover using `AddSigningKeyRotation`.
- Review the role matrix quarterly; automate drift detection with scripts comparing code vs IdP configuration.
DevOps / CI-CD
- Block merge if `roles.json` or policy files change without security-lead approval.
- Run `dotnet user-jwts create --role` in ephemeral environments to smoke-test RBAC in PR pipelines.
Kubernetes & Infrastructure
- Map app roles to K8s ServiceAccounts; deny container exec to non-Admin using PodSecurityPolicies.
- Scan manifests with ARMO or kubernetes-rbac-auditor in CI.
Migration & Legacy
- For existing ACL-based systems, script one-time migration: ACL → CompositeRole → Policy.
- Use feature flags (`IFeatureManager`) to dark-launch new RBAC until coverage = 100%.
Documentation Pattern
- Keep `docs/rbac/README.md` that auto-generates from policy source via `dotnet rbac-docs`. Include diagrams generated by ARMO.
File/Folder Naming Conventions
- roles.json ⟶ declarative role definitions
- policies.yaml ⟶ Cerbos/Casbin policies (if used)
- Authorization/ ⟶ handlers, requirements, constants
- Data/Migrations/Role.sql ⟶ seed core roles into DB
Common Pitfalls & Avoidance
- ❌ Checking roles in the UI only. Always enforce on the server.
- ❌ Storing roles in appsettings for production (leads to drift). Source of truth must be IdP or policy repo.
- ❌ Forgetting 403 → attacker learns existence of resource. Use uniform error bodies + correlation ID.
Ready-to-Use Snippet: Seed Roles on Startup
```csharp
async Task SeedRolesAsync(RoleManager<IdentityRole> roleMgr)
{
foreach (var role in typeof(Roles).GetFields())
{
if (!await roleMgr.RoleExistsAsync(role.GetValue(null)!.ToString()))
await roleMgr.CreateAsync(new IdentityRole(role.GetValue(null)!.ToString()));
}
}
```
Follow these rules to implement RBAC that is secure, testable, auditable, and maintainable across microservices and infrastructure.