HMAC Authentication in ASP.NET Core Web API

How to Implement HMAC Authentication in ASP.NET Core Web API

In this article, I will discuss how to Implement HMAC Authentication in an ASP.NET Core Web API Application with an Example. Please read our previous article on how to Store a Password in Hash Format in the ASP.NET Core Web API Application.

What is HMAC Authentication in ASP.NET Core Web API?

The HMAC stands for Hash-Based Message Authentication Code. From the full form of HMAC, we need to understand two things one is Message Authentication Code and the other one is Hash-Based. So HMAC is a mechanism that is used for creating a Message Authentication Code by using a Hash Function.

The most important point you need to remember is that while generating the Message Authentication Code using the Hash Function, we need to use a Shared Secret Key. Moreover, that Shared Secret Key must be shared between the Client and the Server involved in sending and receiving the data.

So, HMAC (Hash-based Message Authentication Code) is a cryptographic technique used to ensure both the integrity and authenticity of a message. It combines a cryptographic hash function (like SHA-256, SHA-512, or MD5) with a secret key to generate a unique code for a given message. The following are the Key Features of HMAC:

  • Integrity: HMAC ensures that the message has not been altered during transmission. If even a single bit in the message changes, the resulting HMAC value will also change, signaling that the data has been tampered with.
  • Authentication: Since HMAC relies on a secret key known only to the sender and receiver, it provides a way to verify that the message originated from a trusted source. If someone without the secret key tries to generate a valid HMAC, they will fail.
How HMAC Authentication Works?

HMAC ensures that the data has not been tampered with during transmission by creating a unique hash for each HTTP Request based on its content and the secret key. This hash is then sent along with the HTTP Request. Once the Request is received, the server generates its hash from the received message and the secret key. If the received hash matches the generated hash, then it will consider it as a Valid Request. For a better understanding, please have a look at the following diagram:

How HMAC Authentication Works in ASP.NET Core Web API?

Let us understand the step-by-step process for HMAC (Hash-based Message Authentication Code) authentication between a client and a server, which is illustrated in the above diagram.

Step 1: Secret Key Sharing

Before any communication, the client and server share a secret key during the initial setup, which is not transmitted with the requests. This key is kept private and secure on both the client and the server, as it is crucial for generating and verifying the HMAC.

Step 2: Message Preparation by Client

The client prepares the data for HMAC generation. This includes:

  • Message: This combines the HTTP Method, URL Path, and Request Body (if it is a POST, PUT, or PATCH Request).
  • Secret Key: A shared secret key known only to the client and server. This is used to ensure the authenticity of the request.
  • Hash Function: A cryptographic predefined hash function (e.g., SHA-256) that will be used in the HMAC generation. This function will be used by both Client and Server.
  • Timestamp: A timestamp to prevent replay attacks. It ensures that the request is recent.
  • Nonce: A random value used once to ensure that each request is unique. It is used to prevent duplicate requests from being accepted.

These components are used in the HMAC algorithm to create a secure hash that will be sent to the server along with the actual message.

Step 3: HMAC Generation

Using the shared secret key and the hash function agreed upon by both parties, the client computes the HMAC of the combined message components (message, timestamp, nonce). The output is an HMAC signature (cryptographic hash) unique to this request. The resulting HMAC – Message is then included in the HTTP request.

Step 4: Sending the HTTP Request with HMAC

The client sends the actual HTTP request to the server, which includes:

  • Request Data: The HTTP request body or headers.
  • HMAC Header: Contains the generated HMAC, clientid, timestamp, and nonce.

The resulting HMAC is included in the HTTP request’s Authorization header. This header might look like this: Authorization: HMAC ClientId|<computed-hmac>|Nounce|Timestamp. This allows the server to verify the authenticity of the message by comparing the generated HMAC with the one received.

Step 5: The Server Receives the Request

The server receives the incoming HTTP request. The request contains the message (e.g., the body of the request if it is a POST, PUT, or PATCH Request) and the HMAC header, which contains the HMAC value computed by the client, including the Nounce, Client ID, and the Timestamp.

Step 6: Server Prepares for HMAC Regeneration

The server prepares to regenerate the HMAC using the same algorithm. So, upon receiving the request, the server retrieves the following:

  • Message (Request Body): The actual content of the HTTP request.
  • Secret Key: The secret key is already stored and known to the server (it should match the one used by the client).
  • Hash Function: The same cryptographic hash function used to generate the HMAC (e.g., HMAC-SHA256).
  • Timestamp and Nonce: The timestamp and nonce from the HMAC header that was included by the client.
Step 7: Generate HMAC Using the Same Algorithm

The server uses the same HMAC algorithm, secret key, message body, timestamp, and nonce to regenerate the HMAC value. This step ensures that the server can independently compute the HMAC and compare it to the HMAC sent by the client.

Step 8: HMAC Comparison

Once the server has generated the HMAC, it compares the generated HMAC with the HMAC value received from the client in the HTTP request header. This step is necessary to ensure the request has not been tampered with and is authentic.

  • If the HMAC values match, the request is considered valid.
  • If the HMAC values do not match, the request is invalid and is rejected.
Step 9: Request Validation
  • If both HMAC values match, the request is considered valid, and the server can proceed with the requested operation (e.g., performing a transaction or fetching data).
  • If the HMACs do not match, the server rejects the request as invalid, which could mean that the message has been altered or is coming from an unauthorized client.
How to Implement HMAC in ASP.NET Core Web API

Let us understand how to Implement HMAC Authentication in ASP.NET Core Web API. To Implement HMAC in ASP.NET Core Web API, we need to follow the below steps:

  • Create Middleware: First, we need to implement a Custom Middleware component to handle the HMAC authentication logic. This middleware will extract the HMAC Header from the request, reconstruct the message, compute the HMAC, and compare it with the received HMAC.
  • Register Middleware: Then, we need to Register the Custom HMAC middleware Component to the Request Processing Pipeline before the Authorization Middleware to ensure it runs on every request.
  • Client-Side HMAC Generation: Please ensure that the client applications generate the HMAC using the same method, the same logic, and the same secret key and hash function as the server and send the HMAC message in the Request header.

Creating the Server Application

First, let us create the server application to validate the HMAC Authentication. So, please create a new ASP.NET Core Web API application named HMACServerApp. Once you create the project, please install the Entity Framework Core packages by executing the following commands in the Package Manager Console:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Managing the Client Secret:

To manage the secret keys for different clients who are going to consume our Web API services, we need a strategy to securely store and retrieve these keys based on the client making the request.

  • We need to use a secure storage solution such as a database or a secrets manager to store the secret keys for each client. Each client should have a unique identifier (e.g., client ID) that maps to their secret key.
  • When a request is made, extract the client identifier from the request. Retrieve the corresponding secret key from your storage based on the client identifier.
ClientSecret Model:

Create the ClientSecret model to securely store client identifiers and secret keys in the database. So, create a class file named ClientSecrets.cs within the Models folder and then copy and paste the following code:

using System.ComponentModel.DataAnnotations;
namespace HMACServerApp.Models
{
    public class ClientSecret
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [MaxLength(50)]
        public string ClientId { get; set; }

        [Required]
        [MaxLength(200)]
        public string SecretKey { get; set; }
    }
}
Creating Employee Model:

Let us create the Employee model, which we will use to perform the database CRUD operations. So, add a class file named Employee.cs within the Models folder and then copy and paste the following code:

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace HMACServerApp.Models
{
    public class Employee
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [MaxLength(100)]
        public string Name { get; set; }

        [Required]
        [MaxLength(50)]
        public string Position { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal Salary { get; set; }
    }
}
Create the DbContext

Next, we need a context class that inherits from DbContext to manage the entities. So, create a class file named HMACDbContext.cs within the Models folder and then copy and paste the following code:

using Microsoft.EntityFrameworkCore;
namespace HMACServerApp.Models
{
    public class HMACDbContext : DbContext
    {
        public HMACDbContext(DbContextOptions<HMACDbContext> options)
            : base(options)
        {
        }

        public DbSet<Employee> Employees { get; set; }
        public DbSet<ClientSecret> ClientSecrets { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            // Seed initial client secrets with ClientId and SecretKey
            modelBuilder.Entity<ClientSecret>().HasData(
                new ClientSecret { Id = 1, ClientId = "WebAppClient", SecretKey = "a1b2c3d4e5f6g7h8i9j0" },
                new ClientSecret { Id = 2, ClientId = "MobileAppClient", SecretKey = "z9y8x7w6v5u4t3s2r1q0" },
                new ClientSecret { Id = 3, ClientId = "DesktopClient", SecretKey = "m1n2b3v4c5x6z7l8k9j0" }
            );

            // Seed initial employees
            modelBuilder.Entity<Employee>().HasData(
                new Employee { Id = 1, Name = "John Doe", Position = "Software Engineer", Salary = 80000m },
                new Employee { Id = 2, Name = "Jane Smith", Position = "Project Manager", Salary = 95000m }
            );
        }
    }
}
Modify appsettings.json

Make sure to add a connection string to your appsettings.json file. So, please modify the appsettings.json file as follows. We are also storing a key that will tell whether HMAC Authentication is enabled or disabled for your application.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=HMACAuthenticationDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "HMACSettings": {
    "EnableHMAC": true
  }
}
Registering the Connection String and DbContext Instance in Program Class:

Next, we need to configure the connection string and register the context class to use the connection string in the Program class. So, please modify the Program class as follows. Here, we are also telling the Framework to use the property names as defined in the C# model classes.

using HMACServerApp.Models;
using Microsoft.EntityFrameworkCore;

namespace HMACServerApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers()
           .AddJsonOptions(options =>
           {
               // This will use the property names as defined in the C# model
               options.JsonSerializerOptions.PropertyNamingPolicy = null;
           });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            //Configure the ConnectionString and DbContext class
            builder.Services.AddDbContext<HMACDbContext>(options =>
            {
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
            });

            var app = builder.Build();

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

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Database Migration

Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.

HMAC Authentication in ASP.NET Core Web API

With this, our Database with Employees and ClientSecrets tables should be created as shown in the below image:

How to Implement HMAC Authentication in ASP.NET Core Web API

Client Secret Service

Now, we will use a class to check whether a client is valid. If valid, then it will return the associated secret. So, create a class file named ClientSecretService.cs within the Models folder and then copy and paste the following code:

using Microsoft.EntityFrameworkCore;
namespace HMACServerApp.Models
{
    public class ClientSecretService 
    {
        private readonly HMACDbContext _context;

        public ClientSecretService(HMACDbContext context)
        {
            _context = context;
        }

        public async Task<string?> GetSecretKeyAsync(string clientId)
        {
            var client = await _context.ClientSecrets
                .AsNoTracking()
                .FirstOrDefaultAsync(c => c.ClientId == clientId);

            return client?.SecretKey;
        }
    }
}
Middleware for HMAC Authentication in ASP.NET Core Web API:

Let us create a custom middleware component to handle the HMAC Validation for incoming requests. So, create a class file named HMACAuthenticationMiddleware.cs and copy and paste the following code. The following code is self-explained, so please read the comment lines for a better understanding.

using Microsoft.Extensions.Caching.Memory;
using System.Security.Cryptography;
using System.Text;

namespace HMACServerApp.Models
{
    // Middleware class for handling HMAC authentication
    public class HMACAuthenticationMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IMemoryCache _memoryCache;
        private readonly IConfiguration _configuration;

        // Nonce expiry time to prevent reuse of nonces
        private static readonly TimeSpan NonceExpiry = TimeSpan.FromMinutes(5);

        // Constructor to initialize the middleware with the next request delegate and memory cache
        public HMACAuthenticationMiddleware(RequestDelegate next, IMemoryCache memoryCache, IConfiguration configuration)
        {
            _next = next;
            _memoryCache = memoryCache;
            _configuration = configuration;
        }

        // This will handle each request and perform HMAC validation
        public async Task Invoke(HttpContext context)
        {
            // Check if HMAC is enabled
            var isHMACEnabled = _configuration.GetValue<bool>("HMACSettings:EnableHMAC");
            if (!isHMACEnabled)
            {
                // Skip HMAC validation and call the next middleware
                await _next(context);
                return;
            }

            // Proceed with HMAC validation

            // Check if the Authorization header is present
            if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader))
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Authorization header missing");
                return;
            }

            // Check if the Authorization header starts with "HMAC "
            if (!authHeader.ToString().StartsWith("HMAC ", StringComparison.OrdinalIgnoreCase))
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Invalid Authorization header");
                return;
            }

            // Extract token parts from the Authorization header
            var tokenParts = authHeader.ToString().Substring("HMAC ".Length).Trim().Split('|');
            if (tokenParts.Length != 4)
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Invalid HMAC format");
                return;
            }

            var clientId = tokenParts[0]; // Extract client ID
            var token = tokenParts[1];    // Extract HMAC token
            var nonce = tokenParts[2];    // Extract nonce
            var timestamp = tokenParts[3]; // Extract timestamp

            // Resolve the scoped IClientSecretService from the current request's service provider
            var clientSecretService = context.RequestServices.GetRequiredService<ClientSecretService>();

            // Validate the client ID and get the secret key
            var secretKey = await clientSecretService.GetSecretKeyAsync(clientId);
            if (string.IsNullOrEmpty(secretKey))
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                await context.Response.WriteAsync("Invalid client ID");
                return;
            }

            // Validate the timestamp 
            if (!long.TryParse(timestamp, out var timestampSeconds))
            {
                // Invalid timestamp format
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Invalid timestamp format");
                return;
            }

            var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestampSeconds).UtcDateTime;
            var currentTime = DateTime.UtcNow;

            // Check if the timestamp is within the allowed tolerance (NonceExpiry, e.g., 5 minutes)
            if (Math.Abs((currentTime - requestTime).TotalMinutes) > 5)
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Timestamp is outside the allowable range");
                return;
            }

            // Validate the nonce using a client-specific cache key
            var nonceKey = $"{clientId}:{nonce}";
            if (_memoryCache.TryGetValue(nonceKey, out _))
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Nonce has already been used");
                return;
            }

            // Add the client specific nonce to the cache with an expiry time
            _memoryCache.Set(nonceKey, true, NonceExpiry);

            // Read the request body for POST and PUT requests
            var requestBody = string.Empty;
            if (context.Request.Method == HttpMethod.Post.Method || context.Request.Method == HttpMethod.Put.Method)
            {
                //The context.Request.EnableBuffering(); method is used to allow the HTTP request body to be read multiple times.
                //In the context of HMAC authentication middleware, it is necessary because the request body needs to be read to compute the HMAC for validation,
                //but it must also be available to any downstream middleware or the actual API controller.
                context.Request.EnableBuffering();

                //Read the request body
                //Using a StreamReader, we can read the entire request body into a string.
                using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true))
                {
                    //This reads the request body stream to the end.
                    requestBody = await reader.ReadToEndAsync();

                    //The statement context.Request.Body.Position = 0; is used to reset the position of the request body stream to the beginning.
                    //This is important because, by default, the request body stream in ASP.NET Core is a forward-only stream,
                    //meaning once you read it, it cannot be read again unless you explicitly reset its position.
                    context.Request.Body.Position = 0;
                }
            }

            // Validate the HMAC token
            var isValid = ValidateToken(token, nonce, timestamp, context.Request, requestBody, secretKey);

            if (!isValid)
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsync("Invalid HMAC token");
                return;
            }

            // Call the next middleware in the pipeline
            await _next(context);
        }

        // Method to validate the HMAC token
        private bool ValidateToken(string token, string nonce, string timestamp, HttpRequest request, string requestBody, string secretKey)
        {
            var path = Convert.ToString(request.Path);
            var requestContent = new StringBuilder()
                .Append(request.Method.ToUpper())
                .Append(path.ToUpper())
                .Append(nonce)
                .Append(timestamp);

            // Include the request body for POST and PUT methods
            if (request.Method == HttpMethod.Post.Method || request.Method == HttpMethod.Put.Method)
            {
                requestContent.Append(requestBody);
            }

            var secretBytes = Encoding.UTF8.GetBytes(secretKey);
            var requestBytes = Encoding.UTF8.GetBytes(requestContent.ToString());

            // Compute the HMAC using Cryptographic hash function
            using var hmac = new HMACSHA256(secretBytes);
            var computedHash = hmac.ComputeHash(requestBytes);
            var computedToken = Convert.ToBase64String(computedHash);
            return token == computedToken;
        }
    }
}

Note: HMACAuthenticationMiddleware is registered in the application’s middleware pipeline, and middleware instances are created once and treated as singletons by the ASP.NET Core framework. However, the ClientSecretService is a scoped service with a shorter lifetime tied to a single request. So, we have to dynamically resolve the scoped service within the middleware using the IServiceProvider to create a scope for each request.

Registering Memory Cache, Client Secret Service, and HMAC Middleware Components:

Register the IMemoryCache service for the built-in dependency injection container and HMAC Middleware Components for the Application Request Processing Pipeline. So, please modify the Program class as follows:

using HMACServerApp.Models;
using Microsoft.EntityFrameworkCore;

namespace HMACServerApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // This will use the property names as defined in the C# model
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            //Configure the ConnectionString and DbContext class
            builder.Services.AddDbContext<HMACDbContext>(options =>
            {
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
            });

            //Adding In-Memory Caching
            builder.Services.AddMemoryCache();

            // Register the ClientSecretService
            builder.Services.AddScoped<ClientSecretService>();

            var app = builder.Build();

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

            app.UseHttpsRedirection();

            // Registering the HMACAuthenticationMiddleware
            app.UseMiddleware<HMACAuthenticationMiddleware>();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Create Employee Controller

Next, we need to create the Employees Controller, which will expose the endpoints to perform the Database CRUD Operations using the Employee model. So, create an API Empty Controller named EmployeesController within the Controllers folder and then copy and paste the following code:

using HMACServerApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace HMACServerApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        private readonly HMACDbContext _context;

        public EmployeesController(HMACDbContext context)
        {
            _context = context;
        }

        // GET: api/Employees
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Employee>>> GetEmployees()
        {
            return await _context.Employees.AsNoTracking().ToListAsync();
        }

        // GET: api/Employees/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Employee>> GetEmployee(int id)
        {
            var employee = await _context.Employees
                .AsNoTracking()
                .FirstOrDefaultAsync(e => e.Id == id);

            if (employee == null)
            {
                return NotFound(new { Message = $"Employee with ID {id} not found." });
            }

            return employee;
        }

        // POST: api/Employees
        [HttpPost]
        public async Task<ActionResult<Employee>> PostEmployee(Employee employee)
        {
            // Validate the incoming employee data
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            _context.Employees.Add(employee);
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetEmployee), new { id = employee.Id }, employee);
        }

        // PUT: api/Employees/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutEmployee(int id, Employee employee)
        {
            if (id != employee.Id)
            {
                return BadRequest(new { Message = "ID in URL does not match ID in body." });
            }

            // Validate the incoming employee data
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var existingEmployee = await _context.Employees.FindAsync(id);
            if (existingEmployee == null)
            {
                return NotFound(new { Message = $"Employee with ID {id} not found." });
            }

            // Update fields
            existingEmployee.Name = employee.Name;
            existingEmployee.Position = employee.Position;
            existingEmployee.Salary = employee.Salary;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                // Handle concurrency issues if any
                return StatusCode(500, new { Message = "An error occurred while updating the employee." });
            }

            return NoContent();
        }

        // DELETE: api/Employees/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteEmployee(int id)
        {
            var employee = await _context.Employees.FindAsync(id);
            if (employee == null)
            {
                return NotFound(new { Message = $"Employee with ID {id} not found." });
            }

            _context.Employees.Remove(employee);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    }
}

Creating the Client Application:

Let us create the Client Application and Consume the above services using HMAC Authentication. For the Client Application, we will create a Console Application. So, create a .NET Console Application named HMACClientApp.

Creating HMAC Helper Class:

Once you create the project, create a class file named HMACHelper.cs within the Client Console Application and then copy and paste the following code. This class contains one static methodGenerateHmacToken, which will generate the HMAC token. Here, we need to implement the same logic to generate the token we used in the server application to generate the HMAC token.

using System.Security.Cryptography;
using System.Text;

namespace HMACClientApp
{
    public class HMACHelper
    {
        // Method to generate HMAC token
        public static string GenerateHmacToken(string method, string path, string clientId, string secretKey, string requestBody = "")
        {
            // Generate a unique nonce
            var nonce = Guid.NewGuid().ToString();

            // Get the current UTC timestamp as a Unix timestamp
            var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();

            // Build the request content by concatenating method, path, nonce, and timestamp
            var requestContent = new StringBuilder()
                .Append(method.ToUpper())
                .Append(path.ToUpper())
                .Append(nonce)
                .Append(timestamp);

            // If the HTTP method is POST or PUT, append the request body to the request content
            if (method == HttpMethod.Post.Method || method == HttpMethod.Put.Method)
            {
                requestContent.Append(requestBody);
            }

            // Convert secret key and request content to bytes
            var secretBytes = Encoding.UTF8.GetBytes(secretKey);
            var requestBytes = Encoding.UTF8.GetBytes(requestContent.ToString());

            // Create HMACSHA256 instance with the secret key
            using var hmac = new HMACSHA256(secretBytes);

            // Compute the hash of the request content
            var computedHash = hmac.ComputeHash(requestBytes);

            // Convert the computed hash to base64 string (token)
            var computedToken = Convert.ToBase64String(computedHash);

            // Concatenate clientId, computedToken, nonce, and timestamp to form the final token
            return $"{clientId}|{computedToken}|{nonce}|{timestamp}";
        }
    }
}

The above class generates HMAC (Hash-based Message Authentication Code) tokens for securing API requests in a .NET application. The HMAC token is generated based on the HTTP method, request path, client ID, secret key, optional request body, nonce (a unique identifier), and timestamp.

Consuming the Services in Client Application:

Next, modify the Program class as follows. Here, we are consuming the API services by calling the respective endpoints. Also, we are passing the HMAC token using the Authorization header. Please update the Client ID, Secret, and base URL with the URL where your Service Application is running.

using System.Text;

namespace HMACClientApp
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            // Proper Client ID, Secret, and Base URL of the API
            var clientId = "DesktopClient";
            var secretKey = "m1n2b3v4c5x6z7l8k9j0";
            var baseUrl = "https://localhost:7277";

            var client = new HttpClient
            {
                // Default timeout for HttpClient in .NET is 100 seconds; override it here
                Timeout = TimeSpan.FromMinutes(5)
            };

            try
            {
                // 1. Create an Employee (POST Request)
                var employee = new
                {
                    Name = "Pranaya Rout",
                    Position = "Developer",
                    Salary = 60000
                };

                var response = await SendRequestAsync(client, HttpMethod.Post, "/api/employees", clientId, secretKey, baseUrl, employee);
                if (response.IsSuccessStatusCode)
                {
                    // Log success for POST request
                    var responseContent = await response.Content.ReadAsStringAsync();
                    Console.WriteLine("POST Response: Employee Created Successfully");
                    Console.WriteLine($"Response Content: {responseContent}");
                }
                else
                {
                    // Log error details for POST request
                    Console.WriteLine($"POST Error: {response.StatusCode} - {response.ReasonPhrase}");
                }

                // 2. Get All Employees (GET Request)
                response = await SendRequestAsync(client, HttpMethod.Get, "/api/employees", clientId, secretKey, baseUrl);
                if (response.IsSuccessStatusCode)
                {
                    // Log success for GET all employees request
                    var responseContent = await response.Content.ReadAsStringAsync();
                    Console.WriteLine("\nGET Response: Employees Retrieved Successfully");
                    Console.WriteLine($"Response Content: {responseContent}");
                }
                else
                {
                    // Log error details for GET all employees request
                    Console.WriteLine($"GET Error: {response.StatusCode} - {response.ReasonPhrase}");
                }

                // 3. Get Employee by ID (GET Request)
                var employeeId = 1;
                response = await SendRequestAsync(client, HttpMethod.Get, $"/api/employees/{employeeId}", clientId, secretKey, baseUrl);
                if (response.IsSuccessStatusCode)
                {
                    // Log success for GET by ID request
                    var responseContent = await response.Content.ReadAsStringAsync();
                    Console.WriteLine("\nGET by ID Response: Employee Retrieved Successfully");
                    Console.WriteLine($"Response Content: {responseContent}");
                }
                else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
                {
                    // Log not found error for GET by ID request
                    Console.WriteLine($"GET by ID Error: Employee with ID {employeeId} not found.");
                }
                else
                {
                    // Log other error details for GET by ID request
                    Console.WriteLine($"GET by ID Error: {response.StatusCode} - {response.ReasonPhrase}");
                }

                // 4. Update Employee (PUT Request)
                var updatedEmployee = new
                {
                    Id = employeeId,
                    Name = "Rakesh Sharma",
                    Position = "Senior Developer",
                    Salary = 80000
                };

                response = await SendRequestAsync(client, HttpMethod.Put, $"/api/employees/{employeeId}", clientId, secretKey, baseUrl, updatedEmployee);
                if (response.IsSuccessStatusCode)
                {
                    // Log success for PUT request
                    Console.WriteLine($"PUT Response: Employee Updated Successfully. Status: {response.StatusCode}");
                }
                else
                {
                    // Log error details for PUT request
                    Console.WriteLine($"PUT Error: {response.StatusCode} - {response.ReasonPhrase}");
                }

                // 5. Delete Employee (DELETE Request)
                response = await SendRequestAsync(client, HttpMethod.Delete, $"/api/employees/{employeeId}", clientId, secretKey, baseUrl);
                if (response.IsSuccessStatusCode)
                {
                    // Log success for DELETE request
                    Console.WriteLine($"DELETE Response: Employee Deleted Successfully. Status: {response.StatusCode}");
                }
                else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
                {
                    // Log not found error for DELETE request
                    Console.WriteLine($"DELETE Error: Employee with ID {employeeId} not found.");
                }
                else
                {
                    // Log other error details for DELETE request
                    Console.WriteLine($"DELETE Error: {response.StatusCode} - {response.ReasonPhrase}");
                }
            }
            catch (HttpRequestException ex)
            {
                // Log HTTP request-specific exceptions
                Console.WriteLine($"HTTP Request Error: {ex.Message}");
            }
            catch (TaskCanceledException ex)
            {
                // Log timeout exceptions
                Console.WriteLine($"Request Timeout: {ex.Message}");
            }
            catch (Exception ex)
            {
                // Log any unexpected exceptions
                Console.WriteLine($"Unexpected Error: {ex.Message}");
            }

            // Pause before application exits
            Console.ReadKey();
        }

        // Helper method to send an API request.
        // client: HttpClient instance
        // method: HTTP method (GET, POST, PUT, DELETE)
        // endpoint: API endpoint
        // clientId: Client identifier for HMAC
        // secretKey: Secret key for HMAC
        // baseUrl: Base URL of the API
        // data: Data to be serialized as the request body (optional)
        // Returns: HttpResponseMessage returned by the server
        private static async Task<HttpResponseMessage> SendRequestAsync(
            HttpClient client,
            HttpMethod method,
            string endpoint,
            string clientId,
            string secretKey,
            string baseUrl,
            object data = null)
        {
            // Serialize data to JSON if provided
            var requestBody = data != null ? System.Text.Json.JsonSerializer.Serialize(data) : string.Empty;

            // Generate HMAC token for authentication
            var token = HMACHelper.GenerateHmacToken(method.Method, endpoint, clientId, secretKey, requestBody);

            // Construct the HTTP request
            var requestMessage = new HttpRequestMessage(method, $"{baseUrl}{endpoint}")
            {
                // Add request body if applicable (e.g., POST, PUT)
                Content = !string.IsNullOrEmpty(requestBody)
                    ? new StringContent(requestBody, Encoding.UTF8, "application/json")
                    : null
            };

            // Add Authorization header with HMAC token
            requestMessage.Headers.Add("Authorization", $"HMAC {token}");

            // Send the request and return the response
            return await client.SendAsync(requestMessage);
        }
    }
}

With the above changes in place, first run the server application and then run the client application, and you should see that it is working as expected. If everything works as expected, then you will see the following output:

How HMAC Authentication Works?

What is the UNIX Timestamp?

A UNIX timestamp (also known as Epoch time or POSIX time) represents a specific point in time as the number of seconds that have elapsed since January 1, 1970, 00:00:00 UTC (Coordinated Universal Time).

What is a Replay Attack in HMAC Authentication?

A replay attack is a network attack in which a valid data transmission is maliciously or fraudulently repeated or delayed. In the context of HMAC (Hash-based Message Authentication Code) authentication, this can occur when an attacker intercepts a legitimate request with its HMAC signature and then resends it to the server. Since the HMAC signature is valid, if the server does not have mechanisms to recognize that the request has been replayed, it might process the request as if it is a new one, potentially leading to unauthorized actions.

How to Prevent Replay Attacks with HMAC?

To prevent replay attacks using HMAC authentication, we need to implement the following strategies:

  • Timestamps: Include a timestamp in the request header, which the server checks to ensure the request is not too old. This limits the time window during which a captured request can be replayed.
  • Nonce: A nonce (number used once) is a unique number the client includes in each request. The server keeps track of all used nonces to process each request only once.
When Should We Use HMAC in ASP.NET Core Web API?

HMAC is helpful in scenarios where we need to ensure that a message has not been tampered with and is from a trusted source. The following are some specific use cases of HMAC Authentication in ASP.NET Core Web API Application:

  • API Security: When building APIs (Restful Services), especially those exposed over the internet, HMAC ensures that requests are from authorized clients and have not been altered during transmission.
  • Sensitive Data Transmission: When transmitting sensitive data, such as financial or personal information, HMAC provides an additional layer of security by ensuring the integrity and authenticity of the data.
  • Preventing Replay Attacks: Including a timestamp in the HMAC Signature calculation helps protect against replay attacks, where an attacker could resend a captured valid request.
Real-time Example Scenario of HMAC Authentication:

Imagine you have an e-commerce application where clients (mobile apps, web apps) need to access sensitive endpoints like payment processing. You want to ensure that only authorized clients can access these endpoints and that the requests are not tampered with. By implementing HMAC, we add an additional layer of security that verifies both the authenticity and integrity of the requests, thus protecting sensitive operations from potential attacks.

In the next article, I will discuss how to Implement Encryption and Decryption in ASP.NET Core Web API Applications. In this article, I explain How to Implement HMAC Authentication in ASP.NET Core Web API Application with an Example. I hope you enjoy this HMAC Authentication in ASP.NET Core Web API article.

Leave a Reply

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