This is part of series of blogs on JWT and API Documentation
- Mastering JWT Authorization in .NET Core: A Comprehensive Tutorial
- Versioning the APIs
- Maintaining the API documentation in swagger from comments in C# code
1. Mastering JWT Authorization in .NET Core: A Comprehensive Tutorial
JSON Web Tokens (JWTs) provide a secure and efficient way to handle authorization in modern web applications. This tutorial will guide you through setting up a robust JWT authorization system in your .NET Core API, covering general authentication, role-based access control, and the more powerful policy-based authorization.
Table of Contents
- Prerequisites
- Project Setup and Required Packages
- JWT Configuration in
appsettings.json
- Configure JWT Authentication in
Program.cs
- User Authentication and JWT Token Generation
- Models for Login and User
- AuthController for Login and Token Generation
- Securing API Endpoints with
[Authorize]
- General Authorization
- Role-Based Authorization
- Policy-Based Authorization
- Defining Authorization Policies
- Applying Policies in Controllers
- Implementing Custom Policy Requirements (Minimum Age Example)
- Integrating JWT Authentication with Swagger UI
- Invoking APIs with JWT Tokens via Swagger UI
- Best Practices for Effective JWT Authorization
- Troubleshooting 403 Forbidden with Policy-Based Authorization
1. Prerequisites
Before you begin, ensure you have:
- Visual Studio 2022+ or .NET SDK 6.0+ installed. In the tutorial we are using .net 8
- Basic understanding of ASP.NET Core Web API.
- A tool like Postman for API testing (though we’ll focus on Swagger UI).
2. Project Setup and Required Packages
First, create a new ASP.NET Core Web API project.
dotnet new webapi -n JWTSelfAuth
cd JWTSelfAuth
or simply create the .Net Core Web API Project from visual studio as mentioned in the screenshots below.
Next, install the necessary NuGet packages:
Microsoft.AspNetCore.Authentication.JwtBearer
: Provides middleware for JWT bearer authentication.System.IdentityModel.Tokens.Jwt
: For creating and validating JWTs.Microsoft.IdentityModel.Tokens
: Core library for security tokens.
You can install them via the NuGet Package Manager in Visual Studio or using the .NET CLI: Make sure versions are maching with the .Net version
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer -v 8.0.0
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.IdentityModel.Tokens
3. JWT Configuration in appsettings.json
Store your JWT configuration settings in appsettings.json
(or appsettings.Development.json
for development). This includes your secret key, issuer, audience, and token expiration.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "ThisIsMySuperSecretKeyForJWTAuthenticationInDotNetCoreApplication", // CHANGE THIS TO A STRONG, SECURE KEY IN PRODUCTION
"Issuer": "YourAppIssuer",
"Audience": "YourAppAudience",
"ExpirationMinutes": "30" // Token validity in minutes
}
}
Important Considerations:
- Key: This must be a long, complex, and securely stored string. Never hardcode it directly in your code in production environments. Use environment variables, Azure Key Vault, or AWS Secrets Manager.
- Issuer: The entity that issues the token (e.g., your API’s domain).
- Audience: The intended recipient of the token (e.g., your client application’s domain, or your API’s domain if it consumes its own tokens).
- ExpirationMinutes: Defines how long the token is valid. Shorter lifespans are more secure.
4. Configure JWT Authentication in Program.cs
In .NET 6 and later, all service and middleware configuration is typically done in Program.cs
.
using JWTSelfAuth.AuthorizationHelpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(
c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 1safsfsdfdfd\"",
});
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "JWT Authentication",
Version = "v1"
});
// Apply the security requirement to all endpoints
c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
// Configure JWT Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; // Added for completeness
})
.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:Key"]))
};
});
// Configure Policy-Based Authorization
builder.Services.AddAuthorization(options =>
{
// --- Policy 1: Requires a specific role (example of policy encapsulating a role requirement) ---
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin")); // Same as [Authorize(Roles = "Admin")] but as a policy
// --- Policy 2: Requires a specific claim value ---
// First, make sure your JWT generation adds a custom claim, e.g., "permission"
// In AuthController.cs -> GenerateJwtToken, you might add:
// claims.Add(new Claim("permission", "can_view_dashboard"));
// claims.Add(new Claim("permission", "can_edit_products")); // User can have multiple "permission" claims
options.AddPolicy("CanViewDashboard", policy =>
policy.RequireClaim("permission", "can_view_dashboard"));
// --- Policy 3: Requires multiple claim values (AND logic for values within the same claim type) ---
options.AddPolicy("CanManageProducts", policy =>
policy.RequireClaim("permission", "can_create_product", "can_edit_product", "can_delete_product"));
// This means the user MUST have ALL three 'permission' claims.
// If a user only has "can_create_product", this policy would fail.
// --- Policy 4: Requires a minimum age (example of a custom requirement) ---
// This involves creating a custom IAuthorizationRequirement and an IAuthorizationHandler.
// (See next section for detailed implementation of this type of policy)
options.AddPolicy("MinimumAge18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
// --- Policy 5: Combining Role and Claim requirements (AND logic) ---
options.AddPolicy("AdminAndCanViewLogs", policy =>
policy.RequireRole("Admin")
.RequireClaim("permission", "can_view_logs")); // Must be Admin AND have 'can_view_logs' permission
});
builder.Services.AddAuthorization(); // Add authorization services
// Register your custom authorization handler
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication(); // IMPORTANT: This must be before UseAuthorization
app.UseAuthorization(); // IMPORTANT: This must be after UseAuthentication
app.MapControllers();
app.Run();
Explanation of TokenValidationParameters
:
ValidateIssuer
: Ensures the token was issued by the expected entity.ValidateAudience
: Ensures the token is intended for this recipient.ValidateLifetime
: Checks if the token has expired.ValidateIssuerSigningKey
: Verifies the token’s signature using your secret key. This is critical for preventing token tampering.
5. User Authentication and JWT Token Generation
You’ll need an endpoint where users can submit credentials (username/password). If valid, your application will generate and return a JWT.
Models for Login and User
Create these models in your project (e.g., in a Models
folder):
LoginModel.cs
:
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
UserModel.cs
:
public class UserModel
{
public string Username { get; set; }
public string Role { get; set; }
public string DateOfBirth { get; set; } // Added for the MinimumAge policy example
}
AuthController for Login and Token Generation
Create an AuthController.cs
to handle login requests and JWT generation.
using JWTSelfAuth.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWTSelfAuth.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
public AuthController(IConfiguration config)
{
_config = config;
}
[AllowAnonymous] // Allow unauthenticated access to this endpoint
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel login)
{
// Replace with your actual user authentication logic (e.g., database lookup, password hashing)
var user = AuthenticateUser(login);
if (user != null)
{
var token = GenerateJwtTokenRoleBased(user);
return Ok(new { token });
}
return Unauthorized("Invalid credentials.");
}
[AllowAnonymous] // Allow unauthenticated access to this endpoint
[HttpPost("signin")]
public IActionResult Signin([FromBody] LoginModel login)
{
// Replace with your actual user authentication logic (e.g., database lookup, password hashing)
var user = AuthenticateUser(login);
if (user != null)
{
var token = GenerateJwtTokenPolicyBased(user);
return Ok(new { token });
}
return Unauthorized("Invalid credentials.");
}
// This is a placeholder for your actual user authentication
// In a real application, you'd verify against a database, use ASP.NET Core Identity, etc.
private UserModel? AuthenticateUser(LoginModel login)
{
// Example: Hardcoded user for demonstration
if (login.Username == "testuser" && login.Password == "password123")
{
return new UserModel { Username = login.Username, Role = "User" };
}
if (login.Username == "admin" && login.Password == "adminpassword")
{
return new UserModel { Username = login.Username, Role = "Admin" };
}
if (login.Username == "superadmin" && login.Password == "superadminpassword")
{
return new UserModel { Username = login.Username, Role = "Admin,User,Manager" };
}
if (login.Username == "manager" && login.Password == "managerpassword")
{
return new UserModel { Username = login.Username, Role = "Manager" };
}
return null;
}
private string GenerateJwtTokenRoleBased(UserModel user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var roles = user.Role.Split(',').Select(role => role.Trim()).ToList();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Name, user.Username),
// Add user roles as claims
//new Claim(ClaimTypes.Role, user.Role)
};
// Add roles as claims
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
// Alternatively, use a custom claim:
// tokenDescriptor.Subject.AddClaim(new Claim("roles", role));
}
var token = new JwtSecurityToken(
_config["Jwt:Issuer"],
_config["Jwt:Audience"],
claims,
expires: DateTime.Now.AddMinutes(Convert.ToDouble(_config["Jwt:ExpirationMinutes"] ?? "30")), // Token expiry
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private string GenerateJwtTokenPolicyBased(UserModel user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var roles = user.Role.Split(',').Select(role => role.Trim()).ToList();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Name, user.Username),
// Add user roles as claims
//new Claim(ClaimTypes.Role, user.Role)
// Add DateOfBirth for demonstration (replace with actual user data)
new Claim(ClaimTypes.DateOfBirth, "2000-01-15") // Example: User born Jan 15, 2000
};
// Add roles as claims
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
// Alternatively, use a custom claim:
// tokenDescriptor.Subject.AddClaim(new Claim("roles", role));
}
claims.Add(new Claim("permission", "can_view_dashboard"));
claims.Add(new Claim("permission", "can_edit_product")); // User can have multiple "permission" claims
claims.Add(new Claim("permission", "can_create_product")); // User can have multiple "permission" claims
claims.Add(new Claim("permission", "can_delete_product")); // User can have multiple "permission" claims
claims.Add(new Claim("permission", "can_view_logs")); // User can have multiple "permission" claims
var token = new JwtSecurityToken(
_config["Jwt:Issuer"],
_config["Jwt:Audience"],
claims,
expires: DateTime.Now.AddMinutes(Convert.ToDouble(_config["Jwt:ExpirationMinutes"] ?? "30")), // Token expiry
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
6. Securing API Endpoints with [Authorize]
Now you can protect your API endpoints using the [Authorize]
attribute.
General Authorization
The simplest form, requiring just a valid, authenticated token.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
[Route("api/[controller]")]
[ApiController]
[Authorize] // All actions in this controller require authorization
public class ProtectedController : ControllerBase
{
[HttpGet]
public IActionResult GetProtectedData()
{
var username = User.Identity.Name; // Access the authenticated user's name
var role = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; // Access user roles
return Ok($"Hello {username}! You are authenticated and your role is: {role}. This is protected data.");
}
}
Role-Based Authorization
Use the Roles
property of the [Authorize]
attribute to restrict access based on roles defined in the JWT’s ClaimTypes.Role
claim.
// In ProtectedController.cs
// ... (existing code)
[HttpGet("admin-only")]
[Authorize(Roles = "Admin")] // Only users whose JWT contains a ClaimTypes.Role with value "Admin" can access
public IActionResult GetAdminData()
{
return Ok("This data is only accessible by administrators.");
}
[HttpGet("editor-or-admin")]
[Authorize(Roles = "Editor,Admin")] // Users with "Editor" OR "Admin" roles can access
public IActionResult GetEditorOrAdminData()
{
return Ok("This data is accessible by editors or administrators.");
}
[HttpGet("manager")]
// If a user needs multiple roles, you can apply multiple [Authorize] attributes (AND logic)
// Or, for OR logic (like "Editor,Admin"), specify them in a comma-separated list.
[Authorize(Roles = "Manager")]
public IActionResult GetManagerData()
{
return Ok("This is for managers only.");
}
[HttpGet("user-or-admin")]
[Authorize(Roles = "User,Admin")] // Users with "User" or "Admin" roles can access this
public IActionResult GetUserOrAdminData()
{
return Ok("This data is accessible by users or administrators.");
}
Policy-Based Authorization
Policies offer more flexibility by encapsulating various authorization requirements (roles, claims, custom logic).
Defining Authorization Policies
As shown in Program.cs
earlier, policies are defined using builder.Services.AddAuthorization(options => { ... });
.
// In Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
options.AddPolicy("CanViewDashboard", policy => policy.RequireClaim("permission", "can_view_dashboard"));
options.AddPolicy("CanManageProducts", policy =>
policy.RequireClaim("permission", "can_create_product", "can_edit_product", "can_delete_product"));
options.AddPolicy("AdminAndCanViewLogs", policy =>
policy.RequireRole("Admin").RequireClaim("permission", "can_view_logs"));
options.AddPolicy("MinimumAge18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
Applying Policies in Controllers
Use the Policy
property of the [Authorize]
attribute.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace JWTSelfAuth.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ManagementController : ControllerBase
{
[HttpGet("dashboard")]
[Authorize(Policy = "CanViewDashboard")] // Requires the "CanViewDashboard" policy
public IActionResult GetDashboardSummary()
{
var username = User?.Identity?.Name;
var permissions = new List<string>();
permissions = User?.Claims
.Where(c => c.Type == "permission")
.Select(c => c.Value)
.ToList();
return Ok($"Dashboard summary for {username}. Permissions: {string.Join(", ", permissions)}");
}
[HttpPost("products")]
[Authorize(Policy = "CanManageProducts")] // Requires the "CanManageProducts" policy
public IActionResult CreateProduct()
{
return Ok("Product created successfully.");
}
[HttpGet("admin-logs")]
[Authorize(Policy = "AdminAndCanViewLogs")] // Requires the "AdminAndCanViewLogs" policy
public IActionResult GetAdminLogs()
{
return Ok("Admin logs accessed.");
}
[HttpGet("age-restricted")]
[Authorize(Policy = "MinimumAge18")] // Requires the "MinimumAge18" policy
public IActionResult GetAgeRestrictedContent()
{
// In a real scenario, the age would be a claim in the JWT
var userAgeClaim = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.DateOfBirth);
// You'd parse this to DateTime and calculate age for the policy handler
return Ok("Access granted to age-restricted content.");
}
}
}
Implementing Custom Policy Requirements (Minimum Age Example)
For complex authorization logic, you define an IAuthorizationRequirement
and an AuthorizationHandler<TRequirement>
. Create an AuthorizationHelpers Folders and within that create the following files
a. MinimumAgeRequirement.cs
:
using Microsoft.AspNetCore.Authorization;
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
b. MinimumAgeHandler.cs
:
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim == null)
{
// If DOB claim is missing, the user cannot meet the age requirement.
context.Fail();
return Task.CompletedTask;
}
if (DateTime.TryParse(dateOfBirthClaim.Value, out DateTime dateOfBirth))
{
// Calculate age based on a fixed current date (July 16, 2025) for consistency
// In a real app, you would use DateTime.Today or DateTime.UtcNow
var today = new DateTime(2025, 7, 16);
var age = today.Year - dateOfBirth.Year;
// Adjust age if the birthday hasn't occurred yet this year
if (dateOfBirth.Date > today.AddYears(-age)) age--;
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement); // User meets the age requirement
}
else
{
context.Fail(); // User is too young
}
}
else
{
// Claim value is not a valid date format
context.Fail();
}
return Task.CompletedTask;
}
}
c. Register the Handler:
This was already done in Program.cs
under the service registration section:
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
7. Integrating JWT Authentication with Swagger UI
To enable JWT authentication directly within your Swagger UI, you need to configure SwaggerGen services in your Program.cs
.
Step 7.1: Configure SwaggerGen for JWT (in Program.cs
)
This configuration was already included in Step 4. Configure JWT Authentication in Program.cs
for a streamlined setup. It adds the “Authorize” button and “Bearer” security scheme to your Swagger UI.
8. Invoking APIs with JWT Tokens via Swagger UI
When you run your application and navigate to the Swagger UI (usually https://localhost:<YourPort>/swagger
), you’ll see an “Authorize” button (or a padlock icon next to each endpoint) at the top of the page.
Step 8.1: Obtain a JWT Token
- Open Swagger UI: Navigate to
https://localhost:<YourPort>/swagger
. - Locate the Login Endpoint: Find your
AuthController
’sPOST /api/Auth/login
endpoint. Added another end point calledPOST /api/Auth/signin
to identify the difference between role based and policy based tokens, both can be added in a single token - Expand and “Try it out”: Click on the endpoint, then click the “Try it out” button.
- Fill Request Body: In the “Request body” field, enter your login credentials (e.g., for a user who meets the age policy, like
testuser
):{ "username": "testuser", "password": "password123" }
- Execute: Click the “Execute” button.
- Copy the Token: In the “Responses” section, copy the
token
value from the JSON response.
Step 8.2: Authorize Swagger UI with the JWT
- Click the “Authorize” button: This is usually at the top right of the Swagger UI page.
- Paste the Token: In the dialog box that appears, paste your copied JWT token into the “Value” field. The field should automatically prefix it with ` Bearer
. If not, ensure it looks like
Bearer`. - Click “Authorize” (in the dialog) then “Close”.
Step 8.3: Validate Protected Endpoints
Now that Swagger UI is authorized with your token, you can test all your protected endpoints directly:
-
General Protected Endpoint:
- Find the
GET /api/Protected
endpoint. - Click “Try it out”, then “Execute”.
- Expected Result:
200 OK
with “Hello testuser! You are authenticated…”
- Find the
-
Role-Based Endpoint:
- First, get an Admin Token: Go back to
POST /api/Auth/login
, log in asadmin
(admin
/adminpassword
), and copy the new admin token. - Re-authorize Swagger: Click the “Authorize” button again, clear the old token, and paste the admin token.
- Find the
GET /api/Protected/admin-only
endpoint. - Click “Try it out”, then “Execute”.
- Expected Result:
200 OK
with “This data is only accessible by administrators.” - (If you try this with the
testuser
token, you’d get a403 Forbidden
.)
- First, get an Admin Token: Go back to
-
Policy-Based Endpoint (e.g., MinimumAge18):
- Ensure your current token (e.g.,
testuser
token) has aDateOfBirth
claim that satisfies the policy (e.g., “2000-01-15” for an 18+ policy, given the fixed current date of July 16, 2025). - Find the
GET /api/Management/age-restricted
endpoint. - Click “Try it out”, then “Execute”.
- Expected Result:
200 OK
if the token’s DOB meets the policy. - (To test a
403
for this, log in asyounguser
(younguser
/youngpass
), get their token, authorize Swagger with it, then try theage-restricted
endpoint. You should get a403 Forbidden
.)
- Ensure your current token (e.g.,
By following these steps, you can effectively test your JWT-secured API, including role-based and policy-based authorization, entirely within the Swagger UI, providing a much more efficient workflow than external tools for basic testing.
9. Best Practices for Effective JWT Authorization
- HTTPS Only: Always transmit JWTs over HTTPS to prevent interception and token leakage.
- Strong Secret Key: Use a cryptographically strong, long, and complex key. Never hardcode it in production. Use environment variables, Azure Key Vault, or AWS Secrets Manager.
- Short-Lived Access Tokens: Keep access tokens short-lived (e.g., 15-60 minutes). This minimizes the risk if a token is compromised.
- Refresh Tokens: Implement refresh tokens for longer sessions. A long-lived refresh token can be securely exchanged for a new short-lived access token, avoiding frequent user re-logins. Refresh tokens should be stored securely and have a robust revocation mechanism.
- Token Revocation: Since JWTs are stateless, they are valid until they expire. For immediate revocation (e.g., user logout, account compromise, password change), use a blacklist (e.g., Redis cache) to store revoked token IDs (JTI claim) with their original expiry. Before processing any request, check if the token’s JTI is on the blacklist.
- Do Not Store Sensitive Data in JWT Payload: JWTs are base64 encoded, not encrypted. Anyone can decode and read the claims. Only include non-sensitive, publicly available information (e.g., user ID, roles, permissions).
- Validate All Parameters: Always validate the issuer, audience, lifetime, and signing key to ensure the token’s authenticity and integrity.
- Logging: Log token validation failures and authorization attempts for auditing and debugging. This is crucial for identifying potential attacks or misconfigurations.
- Error Handling: Provide clear and informative error messages (e.g., 401 Unauthorized for invalid/missing token, 403 Forbidden for insufficient permissions) to the client.
10. Troubleshooting Common 403 Forbidden with Policy-Based Authorization
A 403 Forbidden
status code means the user is authenticated (their JWT is valid) but is not authorized to access the specific resource because they don’t meet the policy requirements. For the MinimumAge18
policy, this almost always points to the DateOfBirth
claim.
Here’s a systematic approach to debug a 403 with a policy:
-
Inspect the JWT’s Claims (especially
DateOfBirth
):- In
AuthController.cs
: Verify that in yourGenerateJwtToken
method, you are actively adding theClaimTypes.DateOfBirth
claim:claims.Add(new Claim(ClaimTypes.DateOfBirth, user.DateOfBirth));
And that your
AuthenticateUser
method sets a validDateOfBirth
for theUserModel
. - On
jwt.io
: After logging in (e.g., astestuser
oryounguser
), copy the generated JWT and paste it into jwt.io.- In the “Payload” section, look for the claim with the key:
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth
. - Crucially, check its value: Is it present? Is it exactly in
YYYY-MM-DD
format (e.g., “2000-01-15”, “2010-01-15”)? - Based on the fixed date (July 16, 2025) in
MinimumAgeHandler
, a user needs to be born on or before2007-07-16
to be 18 or older. Verify the DOB in the token meets this.
- In the “Payload” section, look for the claim with the key:
- In
-
Debug the
MinimumAgeHandler.cs
:- Set breakpoints inside the
HandleRequirementAsync
method of yourMinimumAgeHandler
. var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
:- Step over this line. Is
dateOfBirthClaim
null
? If so, the claim is missing from the token. This is often the primary reason for a 403. Go back to step 1.
- Step over this line. Is
if (DateTime.TryParse(dateOfBirthClaim.Value, out DateTime dateOfBirth))
:- Step over this. Does
TryParse
returntrue
? If not, the date string format in your claim is incorrect. InspectdateOfBirthClaim.Value
.
- Step over this. Does
var today = new DateTime(2025, 7, 16);
: Confirm this date is correctly set to2025-07-16
.var age = today.Year - dateOfBirth.Year; ... if (dateOfBirth.Date > today.AddYears(-age)) age--;
:- Inspect the
dateOfBirth
variable after parsing. - Step through the age calculation. Verify the calculated
age
is what you expect based ondateOfBirth
andtoday
.
- Inspect the
if (age >= requirement.MinimumAge)
:- Inspect the
age
variable andrequirement.MinimumAge
(which should be 18). - If
age
is less thanrequirement.MinimumAge
, thencontext.Fail()
will be called, leading to the 403. This means the user is genuinely too young based on the DOB provided in their token.
- Inspect the
- Set breakpoints inside the
-
Verify Handler Registration in
Program.cs
:- Ensure this line is present and not commented out:
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
- If the handler isn’t registered, the policy will never be evaluated, and typically authorization will fail (resulting in 403 if default policy is deny or if there’s no matching policy).
- Ensure this line is present and not commented out:
-
Verify Policy Application in Controller:
- Check for typos or case sensitivity issues in the
Policy
name attribute:[Authorize(Policy = "MinimumAge18")] // Policy name MUST match what's defined in Program.cs
- Check for typos or case sensitivity issues in the