ASP.NET Core  

Securing ASP.NET Core Endpoints Using JWT, Claims, and Attribute-Based Policies

Introduction

In microservices applications, different types of users often access the same backend service:

  • Back-Office users – admins, support staff

  • Transporter Panel users – drivers, fleet managers

For security, certain endpoints should be restricted only to Transporter users, and internal logic may require 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.