ASP.NET Core  

Designing Attribute-Based Contextual Authorization in ASP.NET Core Microservices Using JWT Claims

Introduction

Modern microservice architectures rarely deal with simple “logged-in vs not logged-in” authorization. Real-world systems must make authorization decisions based on context — who the user is, which customer they belong to, and which company or tenant they are operating under.

In this article, we will design an enterprise-grade, attribute-based authorization system in ASP.NET Core Web API using JWT claims such as:

  • UserId

  • CustomerId

  • CompanyId

  • UserType

We will:

  • Validate tokens locally (no runtime auth-service calls)

  • Enforce authorization using policies

  • Expose intent using custom attributes

  • Consume contextual data safely inside controllers

  • Keep controllers clean and scalable

This approach is suitable for multi-tenant systems, B2B SaaS platforms, and microservice ecosystems.

Why Attribute-Based Authorization?

Traditional role-based authorization ([Authorize(Roles = "Admin")]) quickly breaks down when:

  • Users operate in multiple companies

  • Access depends on ownership, not just role

  • APIs require different context levels per endpoint

Attributes allow intent to be declared at the endpoint, while policies centralize the rules.

JWT Token Design (Context-Aware)

A context-aware JWT might look like this:

{
  "UserType": "Transporter",
  "UserId": "89",
  "CustomerId": "501",
  "CompanyId": "42",
  "exp": 1770228296
}

Each claim represents authorization context, not UI state.

Authentication Configuration

JWT validation happens locally in each 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("VeryStrongSecretKeyHere"))
        };
    });

This ensures:

  • No network dependency on the auth service

  • High performance

  • Predictable authorization behavior

Defining Authorization Policies

Policies describe rules, not endpoints.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Transporter", policy =>
        policy.RequireClaim("UserType", "Transporter"));

    options.AddPolicy("TransporterWithCustomer", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.HasClaim("UserType", "Transporter") &&
            ctx.User.HasClaim(c => c.Type == "CustomerId")));

    options.AddPolicy("TransporterWithCompany", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.HasClaim("UserType", "Transporter") &&
            ctx.User.HasClaim(c => c.Type == "CompanyId")));

    options.AddPolicy("TransporterWithCustomerAndCompany", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.HasClaim("UserType", "Transporter") &&
            ctx.User.HasClaim(c => c.Type == "CustomerId") &&
            ctx.User.HasClaim(c => c.Type == "CompanyId")));
});

Each policy expresses minimum required context.

Creating Custom Authorization Attributes

Attributes expose policies in a clean, reusable way.

Transporter Only

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class TransporterOnlyAttribute : AuthorizeAttribute
{
    public TransporterOnlyAttribute()
    {
        Policy = "Transporter";
    }
}

Transporter with Customer Context

[AttributeUsage(AttributeTargets.Method)]
public sealed class TransporterWithCustomerAttribute : AuthorizeAttribute
{
    public TransporterWithCustomerAttribute()
    {
        Policy = "TransporterWithCustomer";
    }
}

Transporter with Company Context

[AttributeUsage(AttributeTargets.Method)]
public sealed class TransporterWithCompanyAttribute : AuthorizeAttribute
{
    public TransporterWithCompanyAttribute()
    {
        Policy = "TransporterWithCompany";
    }
}

Full Context Requirement

[AttributeUsage(AttributeTargets.Method)]
public sealed class TransporterWithCustomerAndCompanyAttribute : AuthorizeAttribute
{
    public TransporterWithCustomerAndCompanyAttribute()
    {
        Policy = "TransporterWithCustomerAndCompany";
    }
}

Safe Claim Access (Strongly Typed)

Never scatter FindFirst() across controllers.

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:

  • Prevents null reference bugs

  • Improves readability

  • Centralizes parsing logic

Controller Usage (Method-Level Precision)

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    [HttpGet("customer")]
    [TransporterWithCustomer]
    public IActionResult GetCustomerOrders()
    {
        return Ok(new
        {
            UserId = User.UserId(),
            CustomerId = User.CustomerId()
        });
    }

    [HttpGet("company")]
    [TransporterWithCompany]
    public IActionResult GetCompanyOrders()
    {
        return Ok(new
        {
            UserId = User.UserId(),
            CompanyId = User.CompanyId()
        });
    }

    [HttpPost("full-context")]
    [TransporterWithCustomerAndCompany]
    public IActionResult CreateOrder()
    {
        return Ok(new
        {
            UserId = User.UserId(),
            CustomerId = User.CustomerId(),
            CompanyId = User.CompanyId()
        });
    }
}

Each endpoint clearly states:

  • Who can call it

  • What context is required

Important Architectural Rule

Authorization validates presence of context, not ownership.

Always enforce:

  • User belongs to company

  • Company belongs to customer

  • Operation is allowed for this relationship

This belongs in the service layer, not authorization policies.

Why This Design Scales

  • ✔ Clean separation of concerns

  • ✔ No controller logic duplication

  • ✔ Easy to extend (BranchId, RegionId, ProjectId)

  • ✔ Works across microservices

  • ✔ Fully testable

This pattern is used in enterprise SaaS, multi-tenant platforms, and cloud-native systems.

Final Thoughts

Attribute-based contextual authorization provides:

  • Explicit intent

  • Centralized security rules

  • Clean controllers

  • High scalability

If your system handles multiple identities, tenants, or scopes, this approach is not optional—it is essential.