Previous Domain-Driven Design (DDD) RBAC vs claims-based in ASP.NET Core Next

Authentication, Authorization, and Claims-Based Security in ASP.NET Core

Authentication, Authorization, and Claims-Based Security in ASP.NET Core

This article offers a practical, end-to-end walkthrough of security in ASP.NET Core. You will learn how to authenticate users, authorize their actions, and leverage claims-based security for fine-grained access control. We will cover cookies, JWTs, OpenID Connect, ASP.NET Core Identity, role-based and policy-based authorization, custom authorization requirements, resource-based checks, multi-tenant considerations, testing, and hardening tips. Examples are provided in simple C#.

Security fundamentals

Security in ASP.NET Core is layered. Authentication answers “who are you?”; authorization answers “what are you allowed to do?”; claims express structured facts about the identity. Robust applications separate these concerns: authenticate using reliable identity providers, flow identity as claims, and express access rules using roles, policies, or custom requirements.

  • Authentication: Establishes an identity (e.g., via cookies, JWTs, or OpenID Connect).
  • Authorization: Applies rules to the current identity and the requested operation.
  • Claims: Key-value pairs describing the subject (e.g., sub, email, role, permissions, tenant).
  • Principals and identities: A ClaimsPrincipal can contain multiple ClaimsIdentity instances.

Middleware pipeline and setup

Authentication and authorization are middleware-driven in ASP.NET Core. The order of registration matters. You add services for authentication schemes and policies, then use middleware to apply them on each request.

Minimal hosting: essential pipeline

// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);

// 1) Register authentication schemes and authorization policies
builder.Services.AddAuthentication()
    .AddCookie("AppCookie", opt =>
    {
        opt.LoginPath = "/account/login";
        opt.AccessDeniedPath = "/account/denied";
        opt.SlidingExpiration = true;
        opt.Cookie.Name = ".MyApp.Auth";
        opt.Cookie.HttpOnly = true;
        opt.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        opt.Cookie.SameSite = SameSiteMode.Lax; // Consider Strict for non-SPA
    })
    .AddJwtBearer("ApiJwt", opt =>
    {
        opt.RequireHttpsMetadata = true;
        opt.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://issuer.example.com",
            ValidateAudience = true,
            ValidAudience = "my-api",
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SigningKey"]))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.AddPolicy("AdminsOnly", p => p.RequireRole("Admin"));
    options.AddPolicy("CanEditUser", p => p.RequireClaim("permission", "user.edit"));
});

var app = builder.Build();

// 2) Order: Authentication BEFORE Authorization
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers(); // or MapGet/MapPost handlers for minimal APIs

app.Run();

The fallback policy is an optional safe default that requires authentication for all endpoints unless explicitly annotated with [AllowAnonymous].

Authentication methods

ASP.NET Core supports multiple authentication approaches. The choice depends on app type, deployment topology, client capabilities, and security posture.

  • Cookie authentication: Stateful, typically used by server-rendered web apps (Razor Pages/MVC).
  • JWT bearer: Stateless tokens, the common choice for Web APIs, SPAs, and mobile clients.
  • OpenID Connect (OIDC): Delegated auth via identity providers (e.g., Azure AD, Auth0, Okta, IdentityServer).
  • Certificates: Mutual TLS or client certs for service-to-service scenarios.
  • Windows/Negotiate: For intranet environments integrating with Active Directory.

JWT bearer authentication (APIs)

JWTs are compact, signed tokens containing claims. APIs validate the token signature and claims without server-side state. JWTs are ideal for SPAs, mobile apps, and microservices.

Configure JWT validation

builder.Services.AddAuthentication("ApiJwt")
    .AddJwtBearer("ApiJwt", options =>
    {
        options.RequireHttpsMetadata = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://issuer.example.com",
            ValidateAudience = true,
            ValidAudience = "my-api",
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1),
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SigningKey"]))
        };
    });

Issuing JWTs (auth endpoint)

[AllowAnonymous]
[HttpPost("token")]
public async Task<IActionResult> CreateToken(LoginInput input)
{
    var user = await _users.ValidateAsync(input.Username, input.Password);
    if (user is null) return Unauthorized();

    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim("name", user.DisplayName),
        new Claim("role", user.Role),
        new Claim("tenant", user.TenantId),
        new Claim("permission", "user.edit") // example
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SigningKey"]));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: "https://issuer.example.com",
        audience: "my-api",
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(30),
        signingCredentials: creds);

    var jwt = new JwtSecurityTokenHandler().WriteToken(token);

    return Ok(new { access_token = jwt, token_type = "Bearer", expires_in = 1800 });
}

Protect minimal API endpoints

app.MapGet("/me", (ClaimsPrincipal user) => new
{
    sub = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue(JwtRegisteredClaimNames.Sub),
    name = user.FindFirstValue("name"),
    email = user.FindFirstValue(ClaimTypes.Email),
    roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray()
})
.RequireAuthorization(); // fallback policy requires authentication

app.MapDelete("/users/{id}", (string id) => Results.Ok())
   .RequireAuthorization("CanEditUser");

JWTs should typically be stored in memory on the client (not in localStorage) and sent via the Authorization: Bearer <token> header. For long-lived sessions, use refresh tokens via the auth server, not the API.

OpenID Connect (external identity providers)

OpenID Connect (OIDC) builds on OAuth 2.0 for delegated authentication. Your app redirects users to an identity provider (IdP) for login. On success, the IdP returns an ID token (and often an access token) that your app validates.

Configure OIDC with cookies (hybrid)

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "AppCookie";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("AppCookie")
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://login.example-idp.com";
    options.ClientId = builder.Configuration["Oidc:ClientId"];
    options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
    options.ResponseType = "code"; // Authorization Code Flow
    options.SaveTokens = true; // persist tokens in auth properties
    options.GetClaimsFromUserInfoEndpoint = true;
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("offline_access"); // if refresh tokens are needed
    options.TokenValidationParameters.NameClaimType = "name";
    options.TokenValidationParameters.RoleClaimType = "role";
});

Triggering sign-in and sign-out

public IActionResult SignIn(string returnUrl = "/")
    => Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "oidc");

public IActionResult SignOutUser()
    => SignOut(new AuthenticationProperties { RedirectUri = "/" }, "AppCookie", "oidc");

Use OIDC when you want SSO, MFA, centralized user management, compliance, or federation across multiple apps and APIs.

ASP.NET Core Identity

ASP.NET Core Identity is a membership system that provides user registration, password hashing, account lockout, 2FA, and UI scaffolding. It can be used alone (with cookies) or combined with OIDC (Identity acts as an IdP) or with JWT issuance via IdentityServer-equivalents.

Basic setup

builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

Sign-in with Identity

var result = await _signInManager.PasswordSignInAsync(input.Email, input.Password, input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded) return Redirect("/");
if (result.RequiresTwoFactor) return Redirect("/account/2fa");
if (result.IsLockedOut) return View("Lockout");
return View("Login", new { error = "Invalid login" });

Identity is convenient for greenfield apps needing local accounts and out-of-the-box security flows. For enterprise SSO or B2C, prefer OIDC with a managed IdP.

Authorization models

Authorization is evaluated against the current ClaimsPrincipal. ASP.NET Core offers attributes and imperative checks. You can authorize by role, by claims, using policies, or with custom logic.

Role-based authorization

[Authorize(Roles = "Admin,Support")]
public IActionResult AdminArea() => View();

Policy-based authorization

builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("AdultsOnly", p => p.RequireClaim("age", "18", "19", "20", "21"));
    o.AddPolicy("AdminsOnly", p => p.RequireRole("Admin"));
});
[Authorize(Policy = "AdultsOnly")]
public IActionResult Vote() => View();

Imperative authorization

public class UsersController : Controller
{
    private readonly IAuthorizationService _auth;
    public UsersController(IAuthorizationService auth) => _auth = auth;

    public async Task<IActionResult> Edit(string id)
    {
        var canEdit = await _auth.AuthorizeAsync(User, null, "CanEditUser");
        if (!canEdit.Succeeded) return Forbid();
        return View();
    }
}

Claims-based authorization

Claims-based authorization focuses on statements about the user rather than static roles. This model scales well for dynamic permissions, multi-tenant apps, and external IdPs.

Claims-based authorization evaluates access based on user attributes (claims) rather than only roles. A claim is a key-value pair that expresses something about the user, such as Permission = EditUser or Department = Finance. Policies tie these claims to application rules that are evaluated at runtime.

Require a specific claim

builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("CanEditUser", p => p.RequireClaim("permission", "user.edit"));
});

Multiple acceptable claims (OR)

builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("CanManageUsers", p => p.RequireAssertion(ctx =>
        ctx.User.HasClaim("permission", "user.edit")
        || ctx.User.IsInRole("Admin")));
});

Policy with a required claim

// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanEditUser", policy =>
        policy.RequireClaim("Permission", "EditUser"));
});

// Controller action
[Authorize(Policy = "CanEditUser")]
public IActionResult EditUser(Guid id) => View();

Mapping claims at sign-in

// After validating credentials or receiving tokens from an IdP
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
    new Claim("Permission", "EditUser"),
    new Claim("Department", "Engineering"),
    new Claim("TenantId", "acme") // see multi-tenant section
};

var identity = new ClaimsIdentity(claims, "Cookies");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("Cookies", principal);

Heuristics

  • Expressiveness: Use claims when permissions depend on rich attributes (e.g., country, tier, department).
  • External identity: Prefer claims when integrating with OAuth/OpenID Connect providers that already issue claims.
  • Minimize role explosion: Replace many narrowly scoped roles with permission claims and policies.

Custom requirements and handlers

When policies need logic beyond simple role-or-claim checks, create custom authorization requirements and handlers. A requirement encapsulates the policy rule; a handler enforces it at runtime.

Define a requirement

public sealed class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int Age { get; }
    public MinimumAgeRequirement(int age) => Age = age;
}

Implement a handler

public sealed class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dobClaim = context.User.FindFirst("DateOfBirth");
        if (dobClaim is null) return Task.CompletedTask;

        if (DateTime.TryParse(dobClaim.Value, out var dob))
        {
            var age = (int)((DateTime.UtcNow - dob).TotalDays / 365.2425);
            if (age >= requirement.Age)
                context.Succeed(new MinimumAgeRequirement(requirement.Age));
        }

        return Task.CompletedTask;
    }
}

Register and use the requirement

// Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

// Controller action
[Authorize(Policy = "Over18")]
public IActionResult BuyRestrictedItem() => View();

Tips

  • Keep handlers pure: Prefer reading from user claims and resource data; avoid external I/O inside handlers.
  • Use multiple requirements: Compose policies with several requirements for complex rules.
  • Short-circuit safely: Call context.Succeed only when fully satisfied; never call Fail unless certain.

Resource-based authorization

Resource-based authorization decides access with knowledge of a specific resource instance, such as a document or record. Instead of decorating actions with attributes, you inject and call IAuthorizationService, passing the resource for contextual evaluation.

Model and requirement

public sealed class Document
{
    public Guid Id { get; init; }
    public Guid OwnerId { get; init; }
    public bool IsConfidential { get; init; }
    public string Department { get; init; } = "";
}

public sealed class DocumentOperationRequirement : IAuthorizationRequirement
{
    public string OperationName { get; }
    public DocumentOperationRequirement(string operationName) => OperationName = operationName;
}

Handler using the resource

public sealed class DocumentAuthorizationHandler
    : AuthorizationHandler<DocumentOperationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        DocumentOperationRequirement requirement,
        Document resource)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var dept = context.User.FindFirstValue("Department");

        // Owner can do anything
        if (resource.OwnerId.ToString() == userId)
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // Confidential docs require same department and only "Read"
        if (resource.IsConfidential && requirement.OperationName == "Read" &&
            string.Equals(resource.Department, dept, StringComparison.OrdinalIgnoreCase))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Invoke authorization

public sealed class DocumentsController : Controller
{
    private readonly IAuthorizationService _auth;
    private readonly IDocumentRepository _repo;

    public DocumentsController(IAuthorizationService auth, IDocumentRepository repo)
    { _auth = auth; _repo = repo; }

    public async Task<IActionResult> View(Guid id)
    {
        var doc = await _repo.GetAsync(id);
        if (doc is null) return NotFound();

        var result = await _auth.AuthorizeAsync(User, doc, new DocumentOperationRequirement("Read"));
        if (!result.Succeeded) return Forbid();

        return View(doc);
    }
}

When to prefer resource-based

  • Ownership rules: Access depends on who created or owns the resource.
  • Attribute combinations: Decisions require multiple resource fields (status, department, confidentiality).
  • Externalized checks: You need to consult an external ABAC/PDP service with the resource context.

Multi-tenant and multi-env scenarios

In multi-tenant systems, authorization must isolate data and permissions by tenant. In multi-environment deployments (dev, test, prod), token validation and policies often vary per environment.

Per-tenant claims and scoping

// Example claims
new Claim("TenantId", "acme");
new Claim("TenantRole", "acme:Admin");  // role scoped to a tenant
new Claim("Permission", "acme:Invoices.Read");

// Policy requiring tenant-scoped permission
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("InvoiceRead", p => p.RequireAssertion(ctx =>
        {
            var tenant = ctx.User.FindFirst("TenantId")?.Value;
            return tenant is not null &&
                   ctx.User.HasClaim("Permission", $"{tenant}:Invoices.Read");
        }));
});

Row-level isolation with EF Core

// Resolve TenantId from current user
public interface ICurrentTenant { string TenantId { get; } }

public class AppDbContext : DbContext
{
    private readonly ICurrentTenant _tenant;
    public DbSet<Invoice> Invoices => Set<Invoice>();

    public AppDbContext(DbContextOptions options, ICurrentTenant tenant) : base(options)
        => _tenant = tenant;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Invoice>()
            .HasQueryFilter(i => i.TenantId == _tenant.TenantId);
    }
}

Environment-specific token validation

// appsettings.Development.json vs appsettings.Production.json configure different authorities/audiences
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Jwt:Authority"];
        options.Audience = builder.Configuration["Jwt:Audience"];
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(2)
        };
    });

Advanced: dynamic policy provider

public sealed class PermissionPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) { }

    public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        // Expect policies like "perm:Invoices.Read"
        if (policyName.StartsWith("perm:", StringComparison.OrdinalIgnoreCase))
        {
            var perm = policyName.Substring("perm:".Length);
            var policy = new AuthorizationPolicyBuilder()
                .AddRequirements(new PermissionRequirement(perm))
                .Build();
            return Task.FromResult(policy);
        }
        return base.GetPolicyAsync(policyName);
    }
}

public sealed class PermissionRequirement : IAuthorizationRequirement
{
    public string Permission { get; }
    public PermissionRequirement(string permission) => Permission = permission;
}

With a custom provider and requirement, you can express per-tenant checks like [Authorize("perm:Invoices.Read")] and resolve the current tenant inside the handler to require {tenant}:{permission}.

Pros and cons

Claims-based authorization

  • Pros: Fine-grained control, aligns with external IdPs, reduces role explosion, dynamic policy evaluation.
  • Cons: Requires disciplined claim issuance, can bloat tokens, debugging can be harder than simple roles.

Custom requirements and handlers

  • Pros: Encapsulates complex rules, testable, composable with other requirements.
  • Cons: Overuse can scatter logic, potential performance impact if handlers do I/O.

Resource-based authorization

  • Pros: Context-aware decisions, avoids leaking business rules into attributes, supports ABAC.
  • Cons: More plumbing per action, async flows can be verbose, requires careful repository usage to fetch resources securely.

Multi-tenant patterns

  • Pros: Strong isolation, scalability, per-tenant customization of permissions.
  • Cons: Policy complexity, risk of cross-tenant leaks if filters or claims are misapplied.

When to use which

  • Simple admin panels: Use role-based with a handful of roles; add claims only for exceptions.
  • Enterprise apps with granular permissions: Use claims-based policies with custom requirements for nuanced rules.
  • Per-record ownership or confidentiality: Use resource-based authorization with handlers that inspect the resource.
  • Multi-tenant SaaS: Scope claims by tenant, enforce EF query filters, and centralize permission evaluation in handlers.
  • External identity providers: Prefer claims; translate IdP claims into app permissions during sign-in or via transformation.
  • Highly regulated domains: Combine claims, resource-based checks, and audit logging; keep rules in code-reviewed handlers.

Testing and verification

Unit testing a handler

[Fact]
public async Task Owner_can_read_document()
{
    var doc = new Document { Id = Guid.NewGuid(), OwnerId = Guid.NewGuid(), IsConfidential = true, Department = "HR" };

    var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
    {
        new Claim(ClaimTypes.NameIdentifier, doc.OwnerId.ToString()),
        new Claim("Department", "Finance")
    }, "Test"));

    var requirement = new DocumentOperationRequirement("Read");
    var context = new AuthorizationHandlerContext(new[] { requirement }, user, doc);
    var handler = new DocumentAuthorizationHandler();

    await handler.HandleAsync(context);

    Assert.True(context.HasSucceeded);
}

Integration testing policies

var appFactory = new WebApplicationFactory<Program>()
    .WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            // Optionally stub ICurrentTenant or token validation
        });
    });

var client = appFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TestTokens.WithClaim("Permission", "acme:Invoices.Read"));

var response = await client.GetAsync("/invoices"); // endpoint protected by "InvoiceRead"
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

Manual verification checklist

  • Positive paths: Users with required claims can access all intended endpoints.
  • Negative paths: Users missing a single required claim are denied at every entry point.
  • Resource coverage: Handlers correctly deny access for resources owned by others.
  • Tenant isolation: Users from Tenant A cannot access or enumerate data from Tenant B.
  • Token variance: Different environments reject tokens from the wrong issuer/audience.

Security hardening and pitfalls

  • Validate tokens strictly: Enable ValidateIssuer, ValidateAudience, ValidateIssuerSigningKey, and ValidateLifetime with a small ClockSkew.
  • Do not trust client-sent claims: Only trust claims from validated tokens or your server-side issuance; never accept claims from request bodies.
  • Normalize claim types: Map IdP-specific claims to your canonical types to avoid policy drift.
  • Prevent role explosion: Use permission claims and policies instead of creating dozens of roles.
  • Secure cookies: Set HttpOnly, SecurePolicy = Always, and appropriate SameSite; rotate data protection keys safely.
  • Minimize token scope: Include only necessary claims and use short-lived access tokens plus refresh tokens where applicable.
  • Guard user enumeration: Return generic messages for Access Denied/Not Found to avoid leaking existence of resources.
  • Audit and monitor: Log authorization failures with correlation IDs; alert on suspicious patterns.
  • Tenant boundary defense in depth: Combine claims checks, EF query filters, and route constraints (e.g., /tenants/{tenantId}) for isolation.
  • IdP rotation readiness: Cache JWKS prudently and handle key rollover without downtime.

Example hardened JWT setup

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = config["Jwt:Authority"];
        o.Audience = config["Jwt:Audience"];
        o.RequireHttpsMetadata = true;
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = config["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = config["Jwt:Audience"],
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)
        };
        o.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = ctx => { ctx.NoResult(); return Task.CompletedTask; }
        };
    });

Summary

  • Claims-based policies: Deliver flexible, fine-grained authorization, especially with external IdPs.
  • Custom requirements: Encapsulate complex rules in testable handlers; compose them into policies.
  • Resource-based checks: Authorize with awareness of the specific resource instance for ABAC scenarios.
  • Multi-tenant readiness: Scope claims by tenant, enforce row-level filters, and validate environment-specific tokens.
  • Secure defaults: Validate tokens strictly, minimize token contents, harden cookies, and log denials.
  • Test thoroughly: Unit test handlers, integration test policies, and manually verify tenant and resource isolation.

Glossary

  • Claim: A key-value pair stating something about the user (e.g., Permission = Orders.Read).
  • Policy: A named authorization rule composed of requirements (e.g., “Over18”).
  • Requirement: A condition that must be met for authorization to succeed.
  • Handler: Code that evaluates a requirement against the user and, optionally, a resource.
  • Resource-based authorization: Authorization that considers the specific object being accessed.
  • RBAC: Role-based access control using coarse roles (Admin, Manager).
  • ABAC: Attribute-based access control using user and resource attributes.
  • Tenant: An isolated customer or organization within a multi-tenant system.
  • Environment: A deployment stage (development, test, production) with distinct configuration.
  • IdP: Identity provider that authenticates users and issues tokens/claims.
  • JWT: JSON Web Token used to convey claims securely between parties.
  • EF query filter: An Entity Framework Core global filter that enforces row-level constraints.
Back to Index
Previous Domain-Driven Design (DDD) RBAC vs claims-based in ASP.NET Core Next
*