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 as we worked 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

Implementing JWT (JSON Web Token) authentication in ASP.NET Core Minimal APIs involves setting up middleware to validate tokens and authorize requests based on token claims. Let us understand how to implement JWT Authentication in ASP.NET Core Minimal API with an Example. 

Setting Up JWT Authentication

First, we need to add JWT support to our project, which involves generating tokens for authenticated users and configuring the API to validate these tokens.

Install Necessary Packages

We need Microsoft.AspNetCore.Authentication.JwtBearer package for implementing JWT Authentication. You can install this Package using NuGet Package Manager for the solution or by executing the following command in the Package Manager Console:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

Configure JWT in appsettings.json

Add JWT configuration settings such as the secret key, issuer, and audience 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": {
    "Key": "KHPK6Ucf/zjvU4qW8/vkuuGLHeIo0l9ACJiTaAPLKbk=", //Secret Key
    "Issuer": "https://localhost:7025", //Authentication Server Domain URL Base Address
    "Audience": "https://localhost:7025" //Client Application Domain URL Base Address
  }
}
Add JWT Authentication to the Program.cs File:

Configure the authentication middleware to use JWT in the Program.cs class file, So please add the following code to the Program class file. The following code is self-explained, so please read the comment lines for a better understanding.

// Add JWT Authentication services
// This line begins the configuration of authentication services in the DI container,
// specifying that the default authentication scheme is JwtBearer, commonly used for API security.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // TokenValidationParameters is a class that specifies the parameters
        // that will be used by JwtBearerMiddleware to validate the token in each request.
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // ValidateIssuer set to true means the issuer will be validated during token validation.
            ValidateIssuer = true,
            // ValidateAudience set to true means the audience will be validated during token validation.
            ValidateAudience = true,
            // ValidateLifetime set to true means the token expiry will be checked to ensure it's still valid.
            ValidateLifetime = true,
            // ValidateIssuerSigningKey set to true means the signing key will be validated to ensure the token's integrity.
            ValidateIssuerSigningKey = true,
            // ValidIssuer specifies the issuer to validate. It's taken from the app settings (configuration).
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            // ValidAudience specifies the audience to validate. It's taken from the app settings (configuration).
            ValidAudience = builder.Configuration["Jwt:Audience"],
            // IssuerSigningKey specifies the key used to sign the token. It needs to match the one used to generate the token.
            // It is taken from the app settings and must be a key that both the issuer and the API know.
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

// This setup allows the application to authenticate requests based on the configured JwtBearer settings
// and authorize them based on the configured policies or roles.
builder.Services.AddAuthorization();
Creating Token Generation Service

Create a method to generate JWT tokens, typically after validating user credentials. Add a method to generate tokens. This method can be part of a service that handles authentication. So, create a class file named AuthService.cs within the Models folder and then copy and paste the following code. The AuthService class encapsulates the logic necessary to create a secure token for a given username.

// Import necessary namespaces for JWT tokens, claims, encoding, and security
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

namespace MinimalAPIDemo.Models 
{
    public class AuthService 
    {
        // Declare a private readonly field for the IConfiguration dependency
        private readonly IConfiguration _configuration; 

        // Constructor for the AuthService class that accepts an IConfiguration parameter
        public AuthService(IConfiguration configuration)
        {
            // Initialize the _configuration field with the provided IConfiguration instance
            _configuration = configuration;
        }

        // Method to generate a JWT token for a given username
        public string GenerateJwtToken(string username)
        {
            // Create a new SymmetricSecurityKey using the secret key from configuration.
            // The key is encoded in UTF8 and used for signing the JWT.
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));

            // Create signing credentials using the security key and the HMAC-SHA256 algorithm
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            // Define the claims to be included in the JWT.
            // Claims are name/value pairs that assert information about the subject,
            // such as the username and a unique identifier for the token.
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, username), // Subject claim with the username
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Unique identifier claim with a new GUID
            };

            // Create a new JWT token with the specified issuer, audience, claims, expiration time, and signing credentials
            var token = new JwtSecurityToken(
                issuer: _configuration["Jwt:Issuer"], // 'Issuer' - the party generating the token
                audience: _configuration["Jwt:Audience"], // 'Audience' - the intended recipient of the token
                claims: claims, // Claims contained within the JWT
                expires: DateTime.Now.AddHours(1), // Set the expiration time of the token (1 hour from the current time)
                signingCredentials: credentials); // Credentials used to sign the token, ensuring its validity

            // Serialize the token to a string and return it
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}
Registering the AuthService to the DI Container

Please add the following code to the Program class to register the service to the built-in dependency container.

// Register AuthService in the DI container
builder.Services.AddSingleton<AuthService>();
Endpoint to Generate Tokens

Create an endpoint in your API to authenticate users and generate tokens. So, please add the following endpoint within the Program class to generate the token after validating the user credentials.

// Define an endpoint for token generation
app.MapPost("/authenticate", (AuthService authService, string username, string password) =>
{
    // Here, you should validate the username and password against your storage
    // The provided credentials are hardcoded for demonstration.
    // In a production scenario, you would typically query a database
    // or another secure storage mechanism to validate the credentials.
    // This is a simple example with hardcoded values
    if (username == "user" && password == "pass@123")  
    {
        // If the credentials are valid, generate a JWT using the AuthService.
        var token = authService.GenerateJwtToken(username);

        // If the token is successfully generated, return an HTTP 200 OK response with the token.
        // The token is returned as part of a JSON object with the property name "Token".
        return Results.Ok(new { Token = token });
    }

    // If the credentials are not valid, return an HTTP 401 Unauthorized response.
    // This tells the client that their authentication attempt has failed.
    return Results.Unauthorized();
});
Creating AuthorizationFilter

Implement the AuthorizationFilter to check for valid JWT tokens in requests to secure endpoints. So, please create a class file named AuthorizationFilter.cs within the Models folder and then copy and paste the following code:

namespace MinimalAPIDemo.Models
{
    // Declare the AuthorizationFilter class which implements the IEndpointFilter interface.
    // IEndpointFilter requires implementing InvokeAsync to provide custom processing during the execution of endpoints.
    public class AuthorizationFilter : IEndpointFilter
    {
        // The InvokeAsync method is called by the runtime when the endpoint is executed.
        // It takes the EndpointFilterInvocationContext which provides context for the execution,
        // and the EndpointFilterDelegate which represents the next filter or endpoint action to execute.
        public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
        {
            // Retrieve the user from the HttpContext associated with the current request.
            // HttpContext.User gives access to the security principal associated with this request.
            var user = context.HttpContext.User;

            // Check if the user's identity is authenticated. 
            // The '?' checks if Identity is null before accessing IsAuthenticated to avoid a NullReferenceException.
            // '?? true' ensures that if Identity is null, the expression evaluates to true, treating it as not authenticated.
            if (!user.Identity?.IsAuthenticated ?? true)
            {
                // If the user is not authenticated, return a 401 Unauthorized result immediately.
                // This stops further processing of the request pipeline and sends an unauthorized response to the client.
                return Results.Unauthorized();
            }

            // If the user is authenticated, continue executing the next filter or the endpoint action.
            // 'await next(context)' calls the next delegate in the pipeline, which could be another filter or the actual endpoint delegate.
            // This allows the request to proceed to the actual endpoint logic if all checks pass.
            return await next(context);
        }
    }
}
Apply the Authorization Filter to Secure Endpoints

Apply this filter to any endpoint that requires authorization as follows.

// Define an endpoint to retrieve all employees
app.MapGet("/employees", async (IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        // Log an informational message indicating the start of employee retrieval
        logger.LogInformation("Retrieving all employees");

        // Asynchronously call the service to get all employees
        var employees = await employeeService.GetAllEmployeesAsync();

        // Return a 200 OK response with the list of employees
        return Results.Ok(employees);
    }
    catch (Exception ex)
    {
        // Log the exception with an error message indicating that an error occurred while retrieving employees
        logger.LogError(ex, "An error occurred while retrieving all employees");

        // Return a 500 Internal Server Error response with the exception message
        return Results.Problem(ex.Message);
    }
})
// Add a logging filter to log information about the request and response
.AddEndpointFilter<LoggingFilter>()
// Add an exception handling filter to catch and handle exceptions
.AddEndpointFilter<ExceptionHandlingFilter>()
// Add an authorization filter to ensure the user is authenticated and authorized
.AddEndpointFilter<AuthorizationFilter>();
Complete Program Class Code:

The following is the Complete Program class code:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MinimalAPIDemo.Models;
using System.Text;

// Create a builder for the web application
var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders(); // Clear default providers
builder.Logging.AddConsole(); // Add Console logging provider
builder.Logging.AddDebug(); // Add Debug logging provider

// Add services to the DI container
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register EmployeeService in the DI container
builder.Services.AddScoped<IEmployeeService, EmployeeService>();

// Register AuthService in the DI container
builder.Services.AddSingleton<AuthService>();

// Add services to the container.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add JWT Authentication services
// This line begins the configuration of authentication services in the DI container,
// specifying that the default authentication scheme is JwtBearer, commonly used for API security.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // TokenValidationParameters is a class that specifies the parameters
        // that will be used by JwtBearerMiddleware to validate the token in each request.
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // ValidateIssuer set to true means the issuer will be validated during token validation.
            ValidateIssuer = true,
            // ValidateAudience set to true means the audience will be validated during token validation.
            ValidateAudience = true,
            // ValidateLifetime set to true means the token expiry will be checked to ensure it's still valid.
            ValidateLifetime = true,
            // ValidateIssuerSigningKey set to true means the signing key will be validated to ensure the token's integrity.
            ValidateIssuerSigningKey = true,
            // ValidIssuer specifies the issuer to validate. It's taken from the app settings (configuration).
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            // ValidAudience specifies the audience to validate. It's taken from the app settings (configuration).
            ValidAudience = builder.Configuration["Jwt:Audience"],
            // IssuerSigningKey specifies the key used to sign the token. It needs to match the one used to generate the token.
            // It is taken from the app settings and must be a key that both the issuer and the API know.
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

// This setup allows the application to authenticate requests based on the configured JwtBearer settings
// and authorize them based on the configured policies or roles.
builder.Services.AddAuthorization();

// Build the application
var app = builder.Build();

// Configure the HTTP request pipeline for the development environment
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Define an endpoint for token generation
app.MapPost("/authenticate", (AuthService authService, string username, string password) =>
{
    // Here, you should validate the username and password against your storage
    // The provided credentials are hardcoded for demonstration.
    // In a production scenario, you would typically query a database
    // or another secure storage mechanism to validate the credentials.
    // This is a simple example with hardcoded values
    if (username == "user" && password == "pass@123")  
    {
        // If the credentials are valid, generate a JWT using the AuthService.
        var token = authService.GenerateJwtToken(username);

        // If the token is successfully generated, return an HTTP 200 OK response with the token.
        // The token is returned as part of a JSON object with the property name "Token".
        return Results.Ok(new { Token = token });
    }

    // If the credentials are not valid, return an HTTP 401 Unauthorized response.
    // This tells the client that their authentication attempt has failed.
    return Results.Unauthorized();
});

// CRUD operations for Employee model
// The EmployeeService and ILogger Services are injected into the endpoints

// Define an endpoint to retrieve all employees
app.MapGet("/employees", async (IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        // Log an informational message indicating the start of employee retrieval
        logger.LogInformation("Retrieving all employees");

        // Asynchronously call the service to get all employees
        var employees = await employeeService.GetAllEmployeesAsync();

        // Return a 200 OK response with the list of employees
        return Results.Ok(employees);
    }
    catch (Exception ex)
    {
        // Log the exception with an error message indicating that an error occurred while retrieving employees
        logger.LogError(ex, "An error occurred while retrieving all employees");

        // Return a 500 Internal Server Error response with the exception message
        return Results.Problem(ex.Message);
    }
})
// Add a logging filter to log information about the request and response
.AddEndpointFilter<LoggingFilter>()
// Add an exception handling filter to catch and handle exceptions
.AddEndpointFilter<ExceptionHandlingFilter>()
// Add an authorization filter to ensure the user is authenticated and authorized
.AddEndpointFilter<AuthorizationFilter>();

// Endpoint to retrieve a single employee by their ID
app.MapGet("/employees/{id}", async (int id, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Retrieving employee with ID {Id}", id);
        var employee = await employeeService.GetEmployeeByIdAsync(id);
        if (employee == null)
        {
            logger.LogWarning("Employee with ID {Id} not found", id);
            return Results.NotFound(new { Message = $"Employee with ID {id} not found" });
        }
        return Results.Ok(employee);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while retrieving employee with ID {Id}", id);

        var problemDetails = new ProblemDetails
        {
            Status = 500,
            Title = "An unexpected error occurred.",
            Detail = ex.Message,
            Instance = $"/employees/{id}"
        };

        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>()
.AddEndpointFilter<AuthorizationFilter>();

// Endpoint to create a new employee with validation
app.MapPost("/employees", async (Employee newEmployee, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        if (!ValidationHelper.TryValidate(newEmployee, out var validationResults))
        {
            // Return 400 Bad Request if validation fails
            return Results.BadRequest(validationResults);
        }

        logger.LogInformation("Creating a new employee");
        var createdEmployee =await employeeService.AddEmployeeAsync(newEmployee);
        return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while creating a new employee");
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>()
.AddEndpointFilter<AuthorizationFilter>();

// Endpoint to update an existing employee
app.MapPut("/employees/{id}", async (int id, Employee updatedEmployee, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        if (!ValidationHelper.TryValidate(updatedEmployee, out var validationResults))
        {
            // Return 400 Bad Request if validation fails
            return Results.BadRequest(validationResults);
        }

        logger.LogInformation("Updating employee with ID {Id}", id);
        var employee =await employeeService.UpdateEmployeeAsync(id, updatedEmployee);
        if (employee == null)
        {
            logger.LogWarning("Employee with ID {Id} not found", id);
            return Results.NotFound(new { Message = $"Employee with ID {id} not found" });
        }
        return Results.Ok(employee);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while updating employee with ID {Id}", id);
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>()
.AddEndpointFilter<AuthorizationFilter>();

// Endpoint to delete an employee
app.MapDelete("/employees/{id}", async (int id, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Deleting employee with ID {Id}", id);
        var result = await employeeService.DeleteEmployeeAsync(id);
        if (!result)
        {
            logger.LogWarning("Employee with ID {Id} not found", id);
            return Results.NotFound(new { Message = $"Employee with ID {id} not found" });
        }
        return Results.NoContent();
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while deleting employee with ID {Id}", id);
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>()
.AddEndpointFilter<AuthorizationFilter>();

// Run the application
app.Run();
Explanation
  • JWT Settings in appsettings.json: Stores JWT configuration settings (issuer, audience, key).
  • JWT Authentication Configuration: Configures JWT authentication in the Program.cs using JwtBearerDefaults.AuthenticationScheme.
  • Token Generation Endpoint: Adds an endpoint for generating JWT tokens based on user credentials.
  • Authorization Filter: Checks if the user is authenticated using JWT tokens.
  • Applying Authorization Filter: Uses AddEndpointFilter<AuthorizationFilter>() to secure endpoints.
Testing:

Now, you can test the endpoints, and they should work as expected. First, generate the token using the token endpoint, and then, using the generated token, access the rest of the endpoints.

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 *