Back to: ASP.NET Core Web API Tutorials
JWT Authentication in ASP.NET Core Minimal API
In this article, I will discuss how to implement JWT Authentication in ASP.NET Core Minimal API with examples. Please read our previous articles discussing how to Implement Endpoint Filters in ASP.NET Core Minimal API with Examples. We will be working with the same project that we worked on so far with ASP.NET Core Minimal API. Please read our JWT Authentication in ASP.NET Core Web API article to understand the basic concepts of JWT and how JWT Authentication works in Web API.
JWT Authentication in ASP.NET Core Minimal API Using Endpoint Filters
ASP.NET Core Minimal APIs provide a lightweight approach to building HTTP APIs with minimal code and configuration. When it comes to securing these APIs, JSON Web Token (JWT) Authentication is a modern and widely used approach to ensure stateless, scalable, and secure authorization.
With the introduction of Endpoint Filters in .NET 7+, Minimal APIs offer a clean way to implement cross-cutting concerns, such as authentication and authorization, at the endpoint level.
In this post, you will learn how to implement JWT Authentication in ASP.NET Core Minimal APIs using custom Endpoint Filters that verify and authorize JWT tokens before allowing access to protected endpoints.
What is JWT Authentication?
JWT (JSON Web Token) is an open standard for securely transmitting information between parties as a JSON object. It is commonly used for authenticating and authorizing users in stateless web APIs. Once a user logs in, our API issues a signed token; the client then sends it with each request, allowing the server to validate the token and authorize access.
JSON Web Tokens (JWTs) provide a secure, compact, and stateless method for handling authentication in APIs. In Minimal APIs, Endpoint Filters offer a powerful and clean approach to applying JWT authentication logic around individual endpoints without cluttering the endpoint handlers.
Setting Up JWT Authentication
First, we need to add JWT (JSON Web Token) support to our project, which involves generating tokens for authenticated users and configuring the API to validate these tokens. We need Microsoft.AspNetCore.Authentication.JwtBearer package for implementing JWT Authentication. You can install this Package using NuGet Package Manager for solution or by executing the following command in the Package Manager Console:
- Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Note: Please install the JwtBearer package, which is compatible with your .NET version.
Add JWT configuration in appsettings.json
Add JWT configuration settings such as the secret key and issuer in the appsettings.json file. So, please modify the appsettings.json file as follows:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=MinimalAPIDB;Trusted_Connection=True;TrustServerCertificate=True;" }, "Jwt": { "SecretKey": "e2a313b0dfe334ea626e2f0c0ad00a246c0e6e4508dccfc4ac8902b05875fcdb", //Secret Key "Issuer": "https://localhost:7180" //Authentication Server Domain URL Base Address } }
Token Generation Service Class
Token generation occurs during the login process, where valid user credentials are exchanged for a signed JSON Web Token (JWT) token. The TokenService class handles this by creating a JWT containing claims such as the username and roles, and signing it using a secret key configured in the application. This token encapsulates the user’s identity and authorization claims and is returned to the client after successful login. The client stores this token and includes it in the Authorization header of subsequent API requests. This token-based mechanism enables the server to authenticate requests without maintaining session state, allowing for stateless and scalable authentication.
So, create a class file named TokenService.cs within the Models folder and then copy and paste the following code:
using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace MinimalAPIDemo.Models { public class TokenService { // Holds the app configuration injected via constructor private readonly IConfiguration _configuration; // JWT issuer string (who issues the token) private readonly string _issuer; // Secret key used to sign the token securely private readonly string _secretKey; // Constructor: receives IConfiguration to access app settings public TokenService(IConfiguration configuration) { _configuration = configuration; // Read "Jwt:Issuer" from configuration; throw if missing _issuer = _configuration["Jwt:Issuer"] ?? throw new ArgumentNullException("Jwt:Issuer"); // Read "Jwt:SecretKey" from configuration; throw if missing _secretKey = _configuration["Jwt:SecretKey"] ?? throw new ArgumentNullException("Jwt:SecretKey"); } // Method to generate a JWT token string for a given username and optional extra claims public string GenerateToken(string username, IEnumerable<Claim>? additionalClaims = null) { // Prepare a list of claims - pieces of information about the user var claims = new List<Claim> { // Standard JWT 'sub' (subject) claim with the username new Claim(JwtRegisteredClaimNames.Sub, username), // Name claim from System.Security.Claims representing the user's name new Claim(ClaimTypes.Name, username) }; // If additional claims are provided, add them to the claims list if (additionalClaims != null) claims.AddRange(additionalClaims); // Create a symmetric security key from the secret key string, encoding it as UTF8 bytes var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)); // Create signing credentials using the security key and HMAC SHA256 algorithm var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); // Create the actual JWT token object with issuer, claims, expiry, and signing info var token = new JwtSecurityToken( issuer: _issuer, // Who issued the token claims: claims, // The claims associated with the token (identity info) expires: DateTime.UtcNow.AddHours(1), // Token expiration time (1 hour from now) signingCredentials: creds // The credentials used to sign the token (ensures integrity) ); // Serialize the JWT token object into a compact JWT string format to be sent to clients return new JwtSecurityTokenHandler().WriteToken(token); } } }
Authorization Endpoint Filter
Authorization is the process of determining what an authenticated user is allowed to do. In this application, once a user’s JWT token is validated and their identity established, authorization ensures that only authenticated users can access specific API endpoints. This is implemented by the following JwtAuthorizationEndPointFilter, which checks whether the request has a valid authenticated user. If not authenticated, the filter returns a 401 Unauthorized response, blocking access to protected resources. You can also extend this filter to enforce role-based or claim-based access control, thus restricting sensitive endpoints to users with appropriate permissions. The combination of authentication and authorization guarantees that only valid and permitted users can interact with your API securely.
So, create a class file named JwtAuthorizationEndPointFilter.cs within the Models folder and then copy and paste the following code. This class implements IEndpointFilter to act as an authorization filter for Minimal API endpoints
namespace MinimalAPIDemo.Models { public class JwtAuthorizationEndPointFilter : IEndpointFilter { // This method is called whenever a request hits an endpoint with this filter applied public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { // Obtain the HttpContext from the invocation context to access user and request info var httpContext = context.HttpContext; // Check if the user identity is authenticated // The expression '!httpContext.User.Identity?.IsAuthenticated ?? true' means: // - If User.Identity is null or IsAuthenticated is false, treat as unauthorized (true) if (!httpContext.User.Identity?.IsAuthenticated ?? true) { // If user is NOT authenticated, short-circuit pipeline and return 401 Unauthorized response return Results.Unauthorized(); } // Optional: You can check user roles or claims here to implement fine-grained authorization // Example: deny access if user is not in "Admin" role // if (!httpContext.User.IsInRole("Admin")) // return Results.Forbid(); // Returns 403 Forbidden response // If authenticated (and optionally authorized), proceed to the next filter or endpoint handler return await next(context); } } }
Login DTO
Create a class file named LoginDTO.cs within the Models folder and then copy and paste the following code. The LoginDTO (Data Transfer Object) represents the login credentials sent from the client to the server when a user attempts to authenticate (e.g., through a login API endpoint).
using System.ComponentModel.DataAnnotations; namespace MinimalAPIDemo.Models { public class LoginDTO { // Username is required and must be between 3 and 50 characters long [Required(ErrorMessage = "Username is required.")] [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters.")] public string Username { get; set; } = null!; // Password is required and must be at least 6 characters long [Required(ErrorMessage = "Password is required.")] [MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")] public string Password { get; set; } = null!; } }
Configuring JWT Authentication in Program Class:
In the authentication section, the application is configured to use JSON Web Token (JWT) Bearer authentication. This means that, for protected API endpoints, the server expects each incoming HTTP request to include a valid JWT in its Authorization header. The AddAuthentication and AddJwtBearer methods establish the rules for validating these tokens, including verifying that a trusted issuer issues the token, that it has not expired, and that it has been signed with the server’s secret key.
When a request is received, ASP.NET Core automatically examines the token, verifies its authenticity, and populates the HttpContext.User with the claims from the token if validation succeeds. This process ensures that only authenticated users with a valid token can access endpoints protected by authentication or authorization filters, helping to keep the API secure. Please modify the Program class as follows.
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using MinimalAPIDemo.Models; using System.Security.Claims; using System.Text; namespace MinimalAPIDemo { public class Program { public static void Main(string[] args) { // Create the WebApplication builder which prepares the app with default configs var builder = WebApplication.CreateBuilder(args); // Add Authentication with JWT Bearer builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"] ?? "e2a313b0dfe334ea626e2f0c0ad00a246c0e6e4508dccfc4ac8902b05875fcdb")) }; }); builder.Services.AddAuthorization(); // Configure logging providers builder.Logging.ClearProviders(); // Remove any default logging providers builder.Logging.AddConsole(); // Add console logger (shows logs in terminal) builder.Logging.AddDebug(); // Add debug logger (for IDE/debugging tools) builder.Services.AddMemoryCache(); // Add memory cache service // Configure JSON serialization options for HTTP responses builder.Services.ConfigureHttpJsonOptions(options => { // Disable camelCase conversion; keep property names as declared (PascalCase) options.SerializerOptions.PropertyNamingPolicy = null; }); // Add Swagger/OpenAPI support services for API documentation and UI generation builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Register your repositories and filters builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>(); builder.Services.AddScoped<CachingEndPointFilter>(); builder.Services.AddScoped<ValidationEndPointFilter<Employee>>(); builder.Services.AddScoped<LoggingEndPointFilter>(); //Register ApplicationDbContext builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // Register TokenService and JwtAuthorizeEndpointFilter builder.Services.AddScoped<TokenService>(); builder.Services.AddScoped<JwtAuthorizationEndPointFilter>(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // Register the custom global error handling middleware in the pipeline app.UseMiddleware<ErrorHandlerMiddleware>(); // Add Authentication & Authorization middleware app.UseAuthentication(); app.UseAuthorization(); // Public endpoint: token generation (login) app.MapPost("/login", (LoginDTO login, TokenService tokenService) => { // For demo, use hardcoded username/password validation if (login.Username != "admin" || login.Password != "password") return Results.Unauthorized(); var token = tokenService.GenerateToken(login.Username, new[] { new Claim(ClaimTypes.Role, "Admin") }); return Results.Ok(new { Token = token }); }); // GET /employees - Fetch all employees app.MapGet("/employees", async (IEmployeeRepository repo, ILogger<Program> logger) => { logger.LogInformation("Fetching all employees asynchronously"); var employees = await repo.GetAllEmployeesAsync(); return Results.Ok(employees); }) .AddEndpointFilter<LoggingEndPointFilter>() .AddEndpointFilter<CachingEndPointFilter>(); // GET /employees/{id} - Fetch employee by ID app.MapGet("/employees/{id}", async (int id, IEmployeeRepository repo, ILogger<Program> logger) => { logger.LogInformation($"Fetching employee with ID: {id} asynchronously"); var employee = await repo.GetEmployeeByIdAsync(id); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }) .AddEndpointFilter<LoggingEndPointFilter>() .AddEndpointFilter<JwtAuthorizationEndPointFilter>() .AddEndpointFilter<CachingEndPointFilter>(); // POST /employees - Create a new employee app.MapPost("/employees", async (Employee newEmployee, IEmployeeRepository repo, ILogger<Program> logger) => { //No need to write the Custom Validation logic here var createdEmployee = await repo.AddEmployeeAsync(newEmployee); logger.LogInformation($"Employee created with ID {createdEmployee.Id} asynchronously"); return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee); }) .AddEndpointFilter<LoggingEndPointFilter>() .AddEndpointFilter<JwtAuthorizationEndPointFilter>() .AddEndpointFilter<ValidationEndPointFilter<Employee>>(); // PUT /employees/{id} - Update an existing employee app.MapPut("/employees/{id}", async (int id, Employee updatedEmployee, IEmployeeRepository repo, ILogger<Program> logger) => { var employee = await repo.UpdateEmployeeAsync(id, updatedEmployee); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }) .AddEndpointFilter<LoggingEndPointFilter>() .AddEndpointFilter<JwtAuthorizationEndPointFilter>() .AddEndpointFilter<ValidationEndPointFilter<Employee>>(); // DELETE /employees/{id} - Delete an employee by ID app.MapDelete("/employees/{id}", async (int id, IEmployeeRepository repo, ILogger<Program> logger) => { var deleted = await repo.DeleteEmployeeAsync(id); return deleted ? Results.NoContent() : Results.NotFound(); }) .AddEndpointFilter<LoggingEndPointFilter>() .AddEndpointFilter<JwtAuthorizationEndPointFilter>(); app.Run(); } } }
Testing:
Now, you can test the endpoints, and it should work as expected. First, generate the token by using the token endpoint, and then, using the generated token, you can access the rest of the endpoint.
By combining ASP.NET Core’s built-in JWT support with the flexibility of Endpoint Filters, we achieve a clean, modular, and maintainable approach to securing Minimal API endpoints. This pattern scales well for microservices and small APIs, giving you complete control over authentication logic at the endpoint level.
In the next article, I will discuss how to implement API Versioning in ASP.NET Core Minimal API with Examples. In this article, I explain how to implement JWT Authentication in ASP.NET Core Minimal API with Examples. I hope you enjoy this article, JWT Authentication in ASP.NET Core Minimal API.