Sep 13, 2025
When you make ASP.NET web apps safe, mixing Role-Based Access Control (RBAC) with JSON Web Tokens (JWT) sets up a good way to watch who gets in and manage users. Here's the main point:
Both RBAC and JWT work together to check rights fast, cut down on needing databases, and help the app handle more users. This guide helps you to set up JWT checks, put roles in tokens, and use role-based checks in your ASP.NET app.
Key steps are:
Program.cs.[Authorize] signs to keep to role-based rules.
{
"JwtSecretKey": "Your_Secret_Key_Here"
}
Make use of environment variables to store settings like your JWT secret outside of your project’s central files. It's easy to set them:
By setting these variables, you ensure they won't be leaked if your code makes it to the public, and you can use the same value across different parts of your app.
Before you start coding, double-check that all tools, frameworks, and settings are correct. A good setup now makes your building process smooth. You might face fewer bugs, and the work can be fun not tiresome.
Now, you are ready to make your API. This setup helps keep your app safe and running well. Let's start coding!
{
"Jwt:Secret": "your-super-secret-key-that-is-at-least-32-characters-long",
"Jwt:Issuer": "your-app-name",
"Jwt:Audience": "your-app-users"
}
launchSettings.jsonWhen working locally, you can set these values in launchSettings.json:
{
"profiles": {
"YourApiName": {
"environmentVariables": {
"JWT_SECRET": "your-secret-key-here",
"JWT_ISSUER": "your-app-name",
"JWT_AUDIENCE": "your-app-users"
}
}
}
}
To bring these values into your app, get them through your settings object:
var jwtSecret = configuration["Jwt:Secret"];
var jwtIssuer = configuration["Jwt:Issuer"];
var jwtAudience = configuration["Jwt:Audience"];
In live use, get these data from your set-up tools or the web host's settings for environment data. Tools like Azure App Service and AWS let you add these data securely without showing them in your code.
When your setting is good and safe, you're all set to put in JWT check tools. This set-up makes sure you have a strong base to build a safe API.

Now that your setup is all set, it's time to get a JWT check system going. This means you need to work on settings in the middle, making sure tokens are right, and pulling user info from these tokens to keep access safe.
The first thing to do is to get your ASP.NET app ready for JWT checks. Open your Program.cs file and put in the needed check services along with the JWT right-making settings.
Here's how you can add the check services:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add JWT authentication
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(builder.Configuration["Jwt:Secret"]))
};
});
builder.Services.AddAuthorization();
After setting up the services, make sure to add the middleware in the right order:
var app = builder.Build();
// Add authentication and authorization middleware
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Make sure to put UseAuthentication before UseAuthorization so the check-in steps go in order. After you do that, you can begin to check tokens and get user info.
The setup you just did helps to keep your app safe by looking at each token. The TokenValidationParameters class lays out how you check these tokens:
To make checks better, you can put in your own event actions:
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Console.WriteLine("Token validated successfully");
return Task.CompletedTask;
}
};
});
These event-handlers let you log any issue and check token checks. This makes fixing log-in issues easier.
After a token is checked, you can safely pull user info and roles for more use. In your controllers, you can get to claims through the HttpContext.User property:
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UserController : ControllerBase
{
[HttpGet("profile")]
public IActionResult GetProfile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userName = User.FindFirst(ClaimTypes.Name)?.Value;
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value;
if (userId == null)
{
return Unauthorized("User ID not found in token");
}
return Ok(new
{
Id = userId,
Name = userName,
Email = userEmail
});
}
}
To set who can get in, use roles in ClaimTypes.Role:
[HttpGet("admin-only")]
[Authorize(Roles = "Admin")]
public IActionResult AdminOnlyEndpoint()
{
var userRoles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();
return Ok(new
{
Message = "Welcome, admin!",
UserRoles = userRoles
});
}
To make it easy to work with claims, try making extension methods:
public static class ClaimsPrincipalExtensions
{
public static string GetUserId(this ClaimsPrincipal user)
{
return user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
public static List<string> GetRoles(this ClaimsPrincipal user)
{
return user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();
}
public static bool HasRole(this ClaimsPrincipal user, string role)
{
return user.IsInRole(role);
}
}
You can use these add-ons in your code parts for clear code:
[HttpGet("dashboard")]
[Authorize]
public IActionResult GetDashboard()
{
var userId = User.GetUserId();
var userRoles = User.GetRoles();
if (User.HasRole("Manager"))
{
return Ok("Manager dashboard data");
}
return Ok("Standard user dashboard data");
}
When you deal with custom claims, keep in mind that claim names care about upper or lower case. Stick to the same way of naming them to dodge mistakes.
Now that you have your JWT auth set up, your app can take on safe requests, check tokens, and pull out user info. The next move is to put in role-based rules about who can get to certain API spots.
After you know how to get claims from JWT tokens, your next move is to make a role-based setup for access. This means you put roles into your JWT info, set rules for who can do what, and make sure only certain roles can use certain parts of your API.
When you add roles to the JWT claims, it makes it easier to control access because it keeps the sign-in process simple and cuts down on the need to ask the database for info.
Begin by changing your Identity services to handle roles. Change your Program.cs file like this:
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
The AddRoles<IdentityRole>() method turns on role control for your app. Make sure the Microsoft.AspNetCore.Identity.UI pack is in place.
Next, change how you make tokens to add roles:
public async Task<string> GenerateJwtToken(IdentityUser user)
{
var userRoles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Email, user.Email)
};
foreach (var role in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
When you make each job a ClaimTypes.Role, ASP.NET Core can see and use them for who gets in.
Rules for entry let you set who can do what by mixing many needs. Put these rules in the Program.cs file with the AddAuthorization way:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole",
policy => policy.RequireRole("Administrator"));
options.AddPolicy("RequireManagerOrAdmin",
policy => policy.RequireRole("Manager", "Administrator"));
options.AddPolicy("SeniorStaffOnly", policy =>
{
policy.RequireRole("Manager", "Administrator", "Senior Developer");
policy.RequireAuthenticatedUser();
});
options.AddPolicy("AdultUsersOnly", policy =>
{
policy.RequireClaim("Age");
policy.RequireAssertion(context =>
{
var ageClaim = context.User.FindFirst("Age")?.Value;
return int.TryParse(ageClaim, out int age) && age >= 18;
});
});
});
The RequireRole method lets you set many roles, making a loose OR rule. For harder rules, use RequireAssertion to put in your own logic.
When you have roles and rules set, you can keep your API points safe with the [Authorize] mark. Here is how to use role-based rules in your controllers:
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
[Authorize(Roles = "Administrator")]
[HttpGet("users")]
public IActionResult GetAllUsers()
{
return Ok("List of all users - admin only");
}
[Authorize(Roles = "Administrator,Manager")]
[HttpGet("reports")]
public IActionResult GetReports()
{
return Ok("Management reports");
}
[Authorize(Policy = "SeniorStaffOnly")]
[HttpPost("restricted-operation")]
public IActionResult ExecuteRestrictedOperation()
{
return Ok("Restricted operation completed");
}
}
You can mix ways to check who someone is and what they can do in the same control area:
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class DocumentController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetDocument(int id)
{
return Ok($"Document {id} content");
}
[Authorize(Roles = "Editor,Administrator")]
[HttpPost]
public IActionResult CreateDocument([FromBody] DocumentModel model)
{
return Ok("Document created");
}
[Authorize(Roles = "Administrator")]
[HttpDelete("{id}")]
public IActionResult DeleteDocument(int id)
{
return Ok($"Document {id} deleted");
}
[Authorize(Policy = "RequireManagerOrAdmin")]
[HttpPost("{id}/publish")]
public IActionResult PublishDocument(int id)
{
return Ok($"Document {id} published");
}
}
In fast and changing setups, you can look at roles on the go right inside the action plans:
[HttpGet("dashboard")]
[Authorize]
public IActionResult GetDashboard()
{
if (User.IsInRole("Administrator"))
{
return Ok(new {
Type = "Admin Dashboard",
Data = GetAdminDashboardData(),
UserCount = 1250,
SystemHealth = "Good"
});
}
if (User.IsInRole("Manager"))
{
return Ok(new {
Type = "Manager Dashboard",
Data = GetManagerDashboardData(),
TeamSize = 15
});
}
return Ok(new {
Type = "User Dashboard",
Data = GetUserDashboardData()
});
}
When you set up roles, look at what the users can do, not who they are. Roles like "CanEditPosts" or "CanViewReports" work well and are easy to keep up. They're better than big roles like "Employee" or "Customer."
After you have put RBAC and JWT in place, the next move is to check if your setup works well. Testing makes sure all is good before users face issues, and understanding how to deal with usual problems can help you fix them fast if they go wrong.
To check your RBAC, make test JWT tokens for different roles and try out various situations. Tools like Postman or curl help you send these requests with JWT tokens in the Authorization header.
Begin by making test tokens for different user roles. Here is an easy way in C# to make tokens with specific roles:
private string CreateTestToken(string userId, string email, params string[] roles)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Email, email)
};
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-test-secret-key"));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "test-issuer",
audience: "test-audience",
claims: claims,
expires: DateTime.Now.AddHours(1),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Make different tokens for roles: one for an admin, one for a normal user, and one for none. Then, try requests on your locked places and see what comes back.
aud or iss.Use tokens with right and wrong role flags. For instance, an admin token should open admin-only places well, but a normal user token should get a 403 Can't Go There back. If things don't go as planned, look at the help steps below.
Here are some common trouble spots and how to fix them:
issuer, audience, and secret are the same in your appsettings.json and token making code.GetRolesAsync gets the right roles and that names match your rules.ClockSkew = TimeSpan.FromMinutes(5) in your token check settings for some time wiggle room.RequireRole with many inputs for OR logic, not many calls.After fixing these, think about more logs and checks to better see who gets in and who does not.
Turn on logs to watch who comes in and out on the login and rules side. Set up logs in your Program.cs file like this:
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Debug);
builder.Logging.AddFilter("Microsoft.AspNetCore.Authorization", LogLevel.Debug);
For more clear views, put your own logs in your controllers. Here's a way to track who gets to a key spot:
[Authorize(Roles = "Administrator")]
[HttpGet("sensitive-data")]
public IActionResult GetSensitiveData()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userRoles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);
_logger.LogInformation("User {UserId} with roles [{Roles}] accessed sensitive data",
userId, string.Join(", ", userRoles));
return Ok("Sensitive information");
}
You can also keep track of failed logins to spot any problems:
public class AuthorizationLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AuthorizationLoggingMiddleware> _logger;
public AuthorizationLoggingMiddleware(RequestDelegate next, ILogger<AuthorizationLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
if (context.Response.StatusCode == 403)
{
var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "Anonymous";
var endpoint = context.Request.Path;
_logger.LogWarning("Access denied for user {UserId} to endpoint {Endpoint}", userId, endpoint);
}
}
}
To get deep insights over time, set up a system to check how often people use access data. Keep an eye on things like how often each access point is used, how active each role is, and big jumps in failed access tries. This info will help you make your RBAC setup work better for your users' needs.
To make logs easier to use, pick logging tools that sort things well, like Serilog. Put in details like user IDs, role names, and the paths of access points in your logs to speed up and ease fixing problems.
We locked your ASP.NET API with Role-Based Access Control (RBAC) and JSON Web Tokens (JWT). This way, as your app grows, your security grows too, and it fits many different types of permissions.
Let's go over the big steps we took:
[Authorize(Roles = "RoleName")].It's key to keep everything locked tight. Keep sensitive info like key signs in places like environment settings or config files to stay safe. All these steps help keep your API safe.
Using RBAC and JWT does more than protect your API - it gives big perks over older ways like session-based checks.
With RBAC and JWT set, your API is ready for more cool stuff. ASP.NET Core's system can handle extras like multi-factor checks (MFA). You can add JWT data about MFA status or use outside sign-in helpers like Google or Microsoft. You can do this without risking your safety setup.
This strong safety base is great for new upgrades, letting your API stay safe and flexible as your app gets bigger.
Never put your JWT secret keys right into your code. You should use environment settings or safe spots like cloud Key Management Services (KMS) or Hardware Security Modules (HSMs) instead. These spots offer a secure way to keep key data and ensure only the right parts of your app can get to them.
When you store keys in a secure place, you cut down on the risk of them being stolen and make your app safer from attacks. Always stick to good rules for keeping secrets to protect your API's safety.