JWT Authentication in ASP.NET Core Minimal API

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.

Leave a Reply

Your email address will not be published. Required fields are marked *