Introduction
Authorization in modern ASP.NET Core applications is no longer about simple roles like Admin or User. Enterprise systems require context-aware authorization, where access decisions depend on who the user is, which customer they belong to, and which company or tenant they are operating under.
This article demonstrates a production-grade authorization architecture using:
JWT claims (UserId, CustomerId, CompanyId)
Attribute-based policies
Custom authorization handlers
Strongly typed claim access
Multi-tenant data isolation
Testable and reusable design
This approach is ideal for microservices, SaaS platforms, and B2B systems.
1. Context-Aware JWT Design
A secure JWT should carry authorization context, not UI state.
{
"UserType": "Transporter",
"UserId": "89",
"CustomerId": "501",
"CompanyId": "42",
"exp": 1770228296
}
Each claim answers a question:
| Claim | Purpose |
|---|
| UserId | Who is the caller |
| CustomerId | Which customer scope |
| CompanyId | Which tenant/company |
| UserType | High-level access boundary |
2. JWT Authentication Setup
JWT validation happens locally in every microservice.
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("StrongSecretKey"))
};
});
This removes runtime dependency on the authentication service and improves performance.
3. Authorization Policies (The Rules Engine)
Policies define what must be true for access.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("UserOnly", policy =>
policy.RequireClaim("UserId"));
options.AddPolicy("UserWithCustomer", policy =>
policy.RequireClaim("CustomerId"));
options.AddPolicy("UserWithCompany", policy =>
policy.RequireClaim("CompanyId"));
options.AddPolicy("UserWithCustomerAndCompany", policy =>
policy.RequireClaim("CustomerId")
.RequireClaim("CompanyId"));
});
Policies are reusable, composable, and testable.
4. Attribute-Based API Contracts
Attributes expose authorization intent directly on endpoints.
User + Company Attribute
[AttributeUsage(AttributeTargets.Method)]
public sealed class UserWithCompanyAttribute : AuthorizeAttribute
{
public UserWithCompanyAttribute()
{
Policy = "UserWithCompany";
}
}
User + Customer + Company Attribute
[AttributeUsage(AttributeTargets.Method)]
public sealed class UserWithCustomerAndCompanyAttribute : AuthorizeAttribute
{
public UserWithCustomerAndCompanyAttribute()
{
Policy = "UserWithCustomerAndCompany";
}
}
This keeps controllers declarative and clean.
5. Strongly Typed Claim Access
Avoid repeating FindFirst() everywhere.
public static class IdentityContext
{
public static int UserId(this ClaimsPrincipal user)
=> int.Parse(user.FindFirst("UserId")!.Value);
public static int? CustomerId(this ClaimsPrincipal user)
=> user.FindFirst("CustomerId") is { } c
? int.Parse(c.Value)
: null;
public static int? CompanyId(this ClaimsPrincipal user)
=> user.FindFirst("CompanyId") is { } c
? int.Parse(c.Value)
: null;
}
This pattern:
6. Custom IAuthorizationHandler (Enterprise Control)
Policies validate presence.
Handlers validate relationships.
Requirement
public sealed class CompanyAccessRequirement : IAuthorizationRequirement { }
Handler
public sealed class CompanyAccessHandler
: AuthorizationHandler<CompanyAccessRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
CompanyAccessRequirement requirement)
{
var userId = context.User.FindFirst("UserId")?.Value;
var companyId = context.User.FindFirst("CompanyId")?.Value;
if (!string.IsNullOrEmpty(userId) &&
!string.IsNullOrEmpty(companyId))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Registration
builder.Services.AddSingleton<IAuthorizationHandler, CompanyAccessHandler>();
This enables fine-grained, business-aware authorization.
7. Controller Usage (Method-Level Precision)
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpGet("company")]
[UserWithCompany]
public IActionResult GetCompanyOrders()
{
return Ok(new
{
UserId = User.UserId(),
CompanyId = User.CompanyId()
});
}
[HttpPost("secure")]
[UserWithCustomerAndCompany]
public IActionResult CreateOrder()
{
return Ok(new
{
UserId = User.UserId(),
CustomerId = User.CustomerId(),
CompanyId = User.CompanyId()
});
}
}
Controllers remain thin, focused only on application logic.
8. Multi-Tenant Data Isolation (EF Core)
Prevent cross-tenant data leaks using global query filters.
public class AppDbContext : DbContext
{
private readonly IHttpContextAccessor _http;
public AppDbContext(
DbContextOptions options,
IHttpContextAccessor http) : base(options)
{
_http = http;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var companyId = _http.HttpContext?
.User.FindFirst("CompanyId")?.Value;
if (companyId != null)
{
modelBuilder.Entity<Order>()
.HasQueryFilter(o =>
o.CompanyId == int.Parse(companyId));
}
}
}
This guarantees tenant isolation by default.
9. Unit Testing Authorization
[Fact]
public async Task Should_Authorize_When_CompanyId_Exists()
{
var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim("UserId", "1"),
new Claim("CompanyId", "42")
}, "Test"));
var requirement = new CompanyAccessRequirement();
var context = new AuthorizationHandlerContext(
new[] { requirement }, user, null);
var handler = new CompanyAccessHandler();
await handler.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
Authorization becomes fully testable.
10. Making It NuGet-Ready
This architecture can be packaged into a shared library:
Attributes
Policies
Handlers
Claim helpers
Usage becomes:
services.AddContextAuthorization();
Perfect for large microservice ecosystems.
Final Architecture Rule
Authentication proves identity
Authorization proves permission
Services enforce business rules
Never mix them.
Conclusion
By combining:
You get a secure, scalable, and enterprise-ready authorization system that works across microservices and teams.
This is the pattern used in real-world SaaS and enterprise platforms, not demos.