Protecting ASP.NET Core Endpoints with Claims, JWT, and Attribute-Based Policies

Different user categories frequently use the same backend service in microservices applications:

  • Administrators and support personnel in the back office
  • Drivers and fleet managers that utilize the Transporter Panel

Only Transporter users should be able to access some APIs for security reasons, and internal logic could need the UserId and CompanyId from the JWT token.

This article demonstrates:

  1. Method-level endpoint security for Transporter users
  2. JWT authentication with claims
  3. Attribute-based policies in ASP.NET Core
  4. Accessing UserId and CompanyId in controllers

JWT Token Structure

For Transporter users, the JWT might contain claims like:

{
  "UserType": "Transporter",
  "CompanyId": "1002",
  "UserId": "89",
  "sub": "1001",
  "exp": 1710000000
}
  • UserType differentiates roles (Transporter vs BackOffice)
  • CompanyId identifies the organization
  • UserId uniquely identifies the user

Step 1: Create TransporterWithCompany Attribute

We create a custom attribute to restrict access to Transporter users with a company:

using Microsoft.AspNetCore.Authorization;

namespace MyApp.API.Attributes
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public sealed class TransporterWithCompanyAttribute : AuthorizeAttribute
    {
        public TransporterWithCompanyAttribute()
        {
            Policy = "TransporterWithCompany";
        }
    }
}

This attribute can be applied per method, leaving other endpoints unrestricted.

Step 2: Configure JWT Authentication and Authorization

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using MyApp.API.Attributes;

var builder = WebApplication.CreateBuilder(args);

// Add controllers
builder.Services.AddControllers();

// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// JWT Authentication
var secret = builder.Configuration["Jwt:Secret"];
if (string.IsNullOrWhiteSpace(secret))
    throw new Exception("JWT Secret is missing in configuration.");

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
            ClockSkew = TimeSpan.Zero
        };
    });

// Authorization policy for Transporter users with CompanyId
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TransporterWithCompany", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
        {
            var userType = context.User.FindFirst("UserType")?.Value;
            var companyId = context.User.FindFirst("CompanyId")?.Value;
            return userType == "Transporter" && !string.IsNullOrWhiteSpace(companyId);
        });
    });
});

// Middleware registration
builder.Services.AddScoped<TokenContextMiddleware>();

var app = builder.Build();

// Swagger
app.UseSwagger();
app.UseSwaggerUI();

// HTTPS
app.UseHttpsRedirection();

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

// Middleware to inject UserId and CompanyId into HttpContext.Items
app.UseMiddleware<TokenContextMiddleware>();

app.MapControllers();
app.Run();

appsettings.json example:

{
  "Jwt": {
    "Issuer": "https://example-auth-server.com/",
    "Audience": "https://example-api.com/",
    "Secret": "YourSuperSecretKey123!@#"
  }
}

Step 3: Claims Helper

using System.Security.Claims;

public static class ClaimsHelper
{
    public static string GetUserId(this ClaimsPrincipal user) =>
        user.FindFirst("UserId")?.Value ?? string.Empty;

    public static string GetCompanyId(this ClaimsPrincipal user) =>
        user.FindFirst("CompanyId")?.Value ?? string.Empty;
}

Step 4: Middleware to Inject User Context

using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

public class TokenContextMiddleware
{
    private readonly RequestDelegate _next;

    public TokenContextMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.User.Identity?.IsAuthenticated == true)
        {
            context.Items["UserId"] = context.User.GetUserId();
            context.Items["CompanyId"] = context.User.GetCompanyId();
        }

        await _next(context);
    }
}

Step 5: Sample Controller Using Attribute

using Microsoft.AspNetCore.Mvc;
using MyApp.API.Attributes;

[ApiController]
[Route("api/issue-tracking")]
public class IssueTrackingController : ControllerBase
{
    private readonly IIssueTrackingService _issueTrackingService;

    public IssueTrackingController(IIssueTrackingService issueTrackingService)
    {
        _issueTrackingService = issueTrackingService;
    }

    // Accessible only by Transporter users with CompanyId
    [HttpGet("{id}")]
    [TransporterWithCompany]
    public async Task<IActionResult> GetIssueTrackingById(long id)
    {
        var userId = User.GetUserId();
        var companyId = User.GetCompanyId();

        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(companyId))
            return Unauthorized(new { Message = "Invalid token claims" });

        var issue = await _issueTrackingService.GetByIdAndCompanyAsync(id, companyId);
        if (issue == null) return NotFound();

        return Ok(new
        {
            Issue = issue,
            UserId = userId,
            CompanyId = companyId
        });
    }

    // Public endpoint
    [HttpGet("all")]
    public async Task<IActionResult> GetAllIssueTracking()
    {
        var issues = await _issueTrackingService.GetAllAsync();
        return Ok(issues);
    }
}

Only Transporters with a valid CompanyId can access /api/issue-tracking/{id}. Other users receive 401 Unauthorized.

Step 6: Testing

  1. Generate a JWT token with:
{
  "UserType": "Transporter",
  "CompanyId": "1002",
  "UserId": "89"
}
  1. Call the endpoint using Postman:
GET https://localhost:5001/api/issue-tracking/1
Authorization: Bearer <JWT_TOKEN>
  • Only Transporters with CompanyId succeed
  • Other roles get 401 Unauthorized

Conclusion

This article shows how to:

  • Secure method-level endpoints for Transporter users
  • Extract UserId and CompanyId from JWT claims
  • Use middleware to make claims easily accessible
  • Separate Back-Office and Transporter access cleanly

This approach is scalable, clean, and ready for production microservices.

Best and Most Recommended ASP.NET Core 10.0 Hosting

Fortunately, there are a number of dependable and recommended web hosts available that can help you gain control of your website’s performance and improve your ASP.NET Core 10.0 web ranking. HostForLIFE.eu is highly recommended. In Europe, HostForLIFE.eu is the most popular option for first-time web hosts searching for an affordable plan. Their standard price begins at only €3.49 per month. Customers are permitted to choose quarterly and annual plans based on their preferences. HostForLIFE.eu guarantees “No Hidden Fees” and an industry-leading ’30 Days Cash Back’ policy. Customers who terminate their service within the first thirty days are eligible for a full refund.

By providing reseller hosting accounts, HostForLIFE.eu also gives its consumers the chance to generate income. You can purchase their reseller hosting account, host an unlimited number of websites on it, and even sell some of your hosting space to others. This is one of the most effective methods for making money online. They will take care of all your customers’ hosting needs, so you do not need to fret about hosting-related matters.