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?

HMAC stands for Hash-Based Message Authentication Code. It is a security mechanism used to verify the integrity and authenticity of a message exchanged between a client and a server. It is a cryptographic mechanism that provides two main things:

  • Message Authentication Code (MAC): This is a short piece of information (a code) used to confirm that a message was not altered and indeed came from the expected sender. It ensures message authenticity and integrity.
  • Hash-Based: It uses a hash function such as SHA-256, SHA-512, or MD5 to generate the Message Authentication Code (MAC).

The most important point to keep in mind is that when generating the Message Authentication Code using a Hash Function, a shared secret key must be used. Moreover, that Shared Secret Key must be shared between the Client and the Server involved in sending and receiving the data.

Why do we use HMAC Authentication?

HMAC is especially useful in Web APIs to secure requests from clients and prevent unauthorized access or manipulation. In a typical Web API scenario, requests from clients can be intercepted or tampered with during transmission. To ensure that:

  • The request data has not been altered (data integrity), and
  • The request is genuinely from a trusted client who knows the secret key (authentication),

we use HMAC Authentication.

How HMAC Authentication Works?

HMAC Authentication is a process that ensures the integrity and authenticity of data in HTTP requests between a client and a server. It does this by generating a unique cryptographic signature (called the HMAC) for each request using a shared secret key and the request contents. This hash is then sent along with the HTTP Request. This needs to be done by the client.

The process is carefully designed to prevent attackers from tampering with the request. The server verifies the authenticity of each request by regenerating the HMAC and comparing it with the one sent by the client. For a better understanding, please have a look at the following diagram:

How HMAC Authentication Works?

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 secure communication begins, the client and server initially share a secret key.
  • This secret key is never sent along with requests.
  • It’s stored securely on both sides.

Why? The secret key is the backbone of HMAC authentication. If anyone else gets this key, they could generate valid HMACs and impersonate the client.

Step 2: Message Preparation by Client

The client prepares the data used for generating the HMAC. This data includes:

  • Message: Combination of HTTP method (GET, POST, etc.), URL path, and request body (if any). Example: POST /api/orders {order details}
  • Secret Key: Shared secret key known only to client and server.
  • Hash Function: A cryptographic hash function, such as SHA-256, SHA-512, or MD5, is used to generate the HMAC. Both Client and Server will use the same cryptographic hash function.
  • Timestamp: Current time included to prevent replay attacks (old requests being resent).
  • Nonce: A unique random value generated for every request to ensure uniqueness. It is used to prevent duplicate requests from being accepted.

Why? If someone tries to resend (replay) a previous request, the server will see the same timestamp/nonce and reject it.

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
  • The client uses the shared secret key and hash function to compute an HMAC over the prepared message, timestamp, and nonce.
  • The result is a unique cryptographic hash (HMAC signature) that represents the request.
  • This HMAC signature is then included in the outgoing HTTP request.

Why? Any change in the message, timestamp, nonce, or key will result in a different HMAC. This is how tampering is detected.

Step 4: Sending the HTTP Request with HMAC

The client sends the HTTP request to the server. The request includes:

  • HTTP Request data (body, headers, URL, etc.)
  • HMAC header containing the generated HMAC signature, Client ID (to identify the client), Timestamp, and Nonce.
  • The HMAC is typically included in the Authorization header, for example: Authorization: HMAC ClientId|<HMAC Signature>|Nonce|Timestamp

Why? The server needs these extra values to reconstruct the HMAC itself.

Step 5: Server Receives the Request

The server receives the HTTP request and extracts:

  • The request data (message/body)
  • The HMAC (from the header)
  • The timestamp and nonce
  • The client ID (to look up the correct secret key if there are multiple clients)

Why? These are all necessary to recalculate the HMAC and verify authenticity.

Step 6: Server Prepares for HMAC Regeneration

The server retrieves the following to regenerate the HMAC:

  • The message (request content)
  • The secret key associated with the client ID (stored securely on the server)
  • The same hash function is used by the client
  • The timestamp and nonce from the header

Why? Using the exact same inputs as the client ensures that the recalculated HMAC will match only if no tampering has occurred.

Step 7: Generate HMAC Using Same Algorithm

The server uses the same HMAC algorithm and inputs (message, secret key, timestamp, nonce) to generate an HMAC signature independently.

Why? If the server’s calculation produces the same HMAC as the received one, the message is authentic.

Step 8: HMAC Comparison

The server compares the HMAC it generated to the HMAC sent by the client.

If the signatures match, this means:

  • The request was not altered in transit.
  • The request was generated by a client possessing the secret key.

If they don’t match, it means either:

  • The request was tampered with.
  • The request did not come from an authorized client.
Step 9: Request Validation
  • If both HMAC signatures match, the server accepts and processes the request.
  • If the signatures do not match, the server rejects the request as unauthorized or invalid.
Implementing HMAC Authentication in ASP.NET Core:

To effectively understand and implement HMAC Authentication, we will develop two main applications:

  • Server Application (HMACServerApp): This ASP.NET Core Web API project serves as the backend service, validating incoming requests using HMAC authentication. It will manage client secrets, validate HMAC tokens, and expose secure API endpoints.
  • Client Application (HMACClientApp): This .NET Console Application will act as the consumer of the server API. It will generate HMAC tokens based on the request details and send authenticated requests to the server.

These two applications working together demonstrate the full HMAC flow, secure generation, and validation of request signatures to ensure data integrity and authentication.

Creating the Server (ASP.NET Core Web API) 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, to interact with the database, 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

These packages enable database operations using SQL Server and EF Core tooling support.

Securely Storing and Managing Client Secrets

To securely manage client secret keys:

  • Use a secure storage mechanism, such as a database or secrets manager, to store secret keys.
  • Each client should have a unique identifier (ClientId) mapped to a secret key.
  • When a request arrives, extract the ClientId and fetch its corresponding secret key from storage for validation.

This ensures secret keys are kept safe and can be retrieved dynamically during request validation.

Client Secret Model:

Defines the structure to store client information securely in the database. First, create a folder named Models in the project root directory. Then, 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; } = null!;

        [Required]
        [MaxLength(200)]
        public string SecretKey { get; set; } = null!;
    }
}
Defining Data Models for Secure API Operations:

Let us create the Employee model using which we will perform the Database CRUD Operations. So, create 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; } = null!;

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

        [Column(TypeName = "decimal(18,2)")]
        public decimal Salary { get; set; }
    }
}
Configuring Database Context with Seed Data

Next, we need to create a DbContext class to manage both Employees and Client Secrets. Create a class file named HMACDbContext.cs within the Models folder and copy and paste the following code. Here, we also seed initial data for quick testing (clients and employees).

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 }
            );
        }
    }
}
Setting Up Application Configuration and Connection Strings

Now, we need to store the database connection string and a flag to enable or disable HMAC globally in the appsettings.json file. So, please modify the appsettings.json file as follows.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=HMACAuthenticationDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "HMACSettings": {
    "EnableHMAC": true
  }
}
Implementing ClientSecretService for Secure Key Retrieval

Create a class file named ClientSecretService.cs within the Models folder and then copy and paste the following code. The ClientSecretService class is responsible for retrieving a client’s secret key by ID, using the DbContext. 

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;
        }
    }
}
Validating Requests with HMAC Authentication Middleware

A custom middleware validates incoming requests:

  • Checks if HMAC is enabled.
  • Validates the presence and structure of the Authorization header.
  • Extracts HMAC token, nonce, timestamp, and clientId from headers.
  • Verifies timestamp freshness and nonce uniqueness to prevent replay attacks.
  • Recomputes HMAC signature using the same hashing logic as the client.
  • Allows or rejects the request accordingly.

The middleware carefully reads the request body to compute the HMAC for POST/PUT methods and caches nonces to prevent replay attacks. Create a class file named HMACAuthenticationMiddleware.cs within the Models folder 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
{
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
// Get the IClientSecretService Instance from DI
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;
}
//Convert the timestamp to Unix Time or EPOC Time
var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestampSeconds).UtcDateTime;
var currentTime = DateTime.UtcNow;
// Check if the timestamp is within the allowed timeframe (within 5 minutes)
// This is to avoid Reply Attack
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
// This is to avoid Reply Attack
_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)
{
//Fetch the Request Path
var path = Convert.ToString(request.Path);
// Build the request content by concatenating method, path, nonce, and timestamp
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);
}
// 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);
//compare the generated HMAC with the HMAC received from the request 
return token == computedToken;
}
}
}
Registering Services and Middleware

Now, we need to register the following Middleware and Services into the ASP.NET Core Request Processing Pipeline.

  • Register EF Core DbContext
  • Register IMemoryCache (in-memory caching) for nonce validation.
  • Register ClientSecretService for secret management.
  • Add the custom HMAC middleware before the authorization middleware.

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;
});
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();
}
}
}
API Controller for CRUD Operations

Create an API Empty Controller named EmployeesController within the Controllers folder and then copy and paste the following code. The following Controller provides endpoints for GET, POST, PUT, and DELETE of employees. It uses EF Core for data access and returns appropriate HTTP status codes and messages. All requests to these endpoints are protected by the HMAC middleware.

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();
}
}
}
Migrating and Seeding the Database

Next, we need to generate the Migration and update the database schema. So, open 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 image below:

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

Developing the HMAC-Enabled Client Application:

Create a .NET Console App to consume the API. The client must generate HMAC tokens for each request using the exact same algorithm and message formatting as the server. All requests must include the token in the Authorization header. So, create a .NET Console Application named HMACClientApp.

Creating HMAC Helper Class to Generate Tokens and Send HTTP Request on the Client:

Once you create the .NET Console Application project, then create a class file named HMACHelper.cs and copy and paste the following code. This class contains two static methods. The GenerateHmacToken will generate the HMAC token. Here, we need to implement the same logic to generate the token that we used in the server application to generate the HMAC token. The SendRequestAsync method will send an HTTP request to the server.

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}";
}
// Helper method to send an API request and returns HttpResponseMessage
public static async Task<HttpResponseMessage> SendRequestAsync(
HttpClient client,    // HttpClient instance
HttpMethod method,    // HTTP method (GET, POST, PUT, DELETE)
string baseUrl,       // Base URL of the API
string endpoint,      // API endpoint
string clientId,      // Client identifier for HMAC
string secretKey,     // Secret key for HMAC
object? data = null)  // Optional Data
{
// 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);
}
}
}
Consuming API Endpoints with HMAC:

Next, modify the Program class as follows. The client app demonstrates making POST, GET, PUT, and DELETE requests, always adding the correct HMAC Authorization header. Error handling for all possible failures (invalid token, not found, timeout, etc.) is included. Please update the Client ID, Secret, and base URL with the URL where your Service Application is running.

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:7035";
var client = new HttpClient
{
// Default timeout for HttpClient in .NET is 100 seconds;
// override it here to 5 Minutes
Timeout = TimeSpan.FromMinutes(5)
};
try
{
// Create a New Employee (POST Request)
var employee = new
{
Name = "Pranaya Rout",
Position = "Developer",
Salary = 60000
};
var response = await HMACHelper.SendRequestAsync(client, HttpMethod.Post, baseUrl, "/api/employees", clientId, secretKey, 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}");
}
// Get All Employees (GET Request)
response = await HMACHelper.SendRequestAsync(client, HttpMethod.Get, baseUrl, "/api/employees", clientId, secretKey);
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}");
}
// Get Employee by ID (GET Request)
var employeeId = 1;
response = await HMACHelper.SendRequestAsync(client, HttpMethod.Get, baseUrl, $"/api/employees/{employeeId}", clientId, secretKey);
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}");
}
// Update Employee (PUT Request)
var updatedEmployee = new
{
Id = employeeId,
Name = "Rakesh Sharma",
Position = "Senior Developer",
Salary = 80000
};
response = await HMACHelper.SendRequestAsync(client, HttpMethod.Put, baseUrl, $"/api/employees/{employeeId}", clientId, secretKey, 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}");
}
// Delete Employee(DELETE Request)
response = await HMACHelper.SendRequestAsync(client, HttpMethod.Delete, baseUrl, $"/api/employees/{employeeId}", clientId, secretKey);
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 (Exception ex)
{
// Log any unexpected exceptions
Console.WriteLine($"Unexpected Error: {ex.Message}");
}
Console.ReadKey();
}
}
}

With the above changes in place, first run the Server application, 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:

Developing the HMAC-Enabled Client Application

What is a UNIX Timestamp?

A UNIX timestamp (Epoch time) is a numeric representation of time as the number of seconds since January 1, 1970, 00:00:00 UTC. For example, 1718170871 → corresponds to a specific moment in time. Used in HMAC for:

  • Ensuring the freshness of requests.
  • Preventing replay attacks.
What is Replay Attack in HMAC Authentication?

A replay attack is when an attacker captures a valid request and resends it to the server to fraudulently repeat the operation. Since the HMAC signature is valid, the server might accept it if it is not protected.

Prevention Strategies:

  • Timestamps: Only accept requests within a short time window.
  • Nonce: Store recently used nonces and reject duplicates.

Together, these ensure each request is unique and recent.

Testing Replay Attacks and Request Tampering Scenarios with HMAC:

Let’s extend the Program class in our HMACClientApp to demonstrate:

  • Valid POST Request (should succeed)
  • Replay Attack (identical request resent with same HMAC– should fail)
  • Tampered Body Replay (use the same HMAC, but modify body – should fail)
  • Replay with Different Nonce/Timestamp (same HMAC with Different Nonce/Timestamp, should fail)

So, please modify the Program class of your Console Application as follows:

using System.Text;
namespace HMACClientApp
{
public class Program
{
static async Task Main(string[] args)
{
var clientId = "DesktopClient";
var secretKey = "m1n2b3v4c5x6z7l8k9j0";
var baseUrl = "https://localhost:7035";
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
var method = HttpMethod.Post.Method;
var path = "/api/employees";
var employee = new
{
Name = "Pranaya Rout",
Position = "Developer",
Salary = 60000
};
var requestBody = System.Text.Json.JsonSerializer.Serialize(employee);
// Valid POST Request (should succeed)
Console.WriteLine("1. Valid POST Request (should succeed)");
// Generate token using helper (nonce & timestamp internally generated)
var validToken = HMACHelper.GenerateHmacToken(method, path, clientId, secretKey, requestBody);
// Extract nonce and timestamp from validToken (format: ClientId|Token|Nonce|Timestamp)
var tokenParts = validToken.Split('|');
if (tokenParts.Length != 4)
{
Console.WriteLine("Error: Invalid token format.");
return;
}
var nonce = tokenParts[2];
var timestamp = tokenParts[3];
var response = await SendHttpRequest(client, method, baseUrl, path, validToken, requestBody);
Console.WriteLine($"Status: {response.StatusCode} | Response: {await response.Content.ReadAsStringAsync()}\n");
// Replay Attack (identical request with same HMAC - should fail)
Console.WriteLine("2. Replay Attack (identical request with same HMAC) (should fail)");
response = await SendHttpRequest(client, method, baseUrl, path, validToken, requestBody);
Console.WriteLine($"Status: {response.StatusCode} | Response: {await response.Content.ReadAsStringAsync()}\n");
// Tampered Body Replay (same HMAC but modified body - should fail)
Console.WriteLine("3. Tampered Body Replay (same HMAC but modified body) (should fail)");
var tamperedBody = System.Text.Json.JsonSerializer.Serialize(new
{
Name = "Hacker",
Position = "Intruder",
Salary = 999999
});
response = await SendHttpRequest(client, method, baseUrl, path, validToken, tamperedBody);
Console.WriteLine($"Status: {response.StatusCode} | Response: {await response.Content.ReadAsStringAsync()}\n");
// Replay with Different Nonce/Timestamp (same HMAC but different nonce/timestamp - should fail)
Console.WriteLine("4. Replay with Different Nonce/Timestamp but SAME HMAC (should fail)");
var differentNonce = Guid.NewGuid().ToString();
var differentTimestamp = DateTimeOffset.UtcNow.AddSeconds(10).ToUnixTimeSeconds().ToString();
// Rebuild token string replacing nonce and timestamp but keeping original signature
var tamperedToken = $"{tokenParts[0]}|{tokenParts[1]}|{differentNonce}|{differentTimestamp}";
response = await SendHttpRequest(client, method, baseUrl, path, tamperedToken, requestBody);
Console.WriteLine($"Status: {response.StatusCode} | Response: {await response.Content.ReadAsStringAsync()}\n");
Console.WriteLine("Test scenarios completed.");
Console.ReadKey();
}
private static async Task<HttpResponseMessage> SendHttpRequest(
HttpClient client,
string method,
string baseUrl,
string endpoint,
string authToken,
string requestBody)
{
var httpMethod = new HttpMethod(method);
var requestMessage = new HttpRequestMessage(httpMethod, $"{baseUrl}{endpoint}");
// Add request body if method is POST or PUT
if ((method == HttpMethod.Post.Method || method == HttpMethod.Put.Method) && !string.IsNullOrEmpty(requestBody))
{
requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
}
// Add Authorization header with manually crafted token
requestMessage.Headers.Remove("Authorization");
requestMessage.Headers.Add("Authorization", $"HMAC {authToken}");
return await client.SendAsync(requestMessage);
}
}
}
Output:

Testing Replay Attacks and Request Tampering Scenarios with HMAC

Implementing HMAC Authentication in your ASP.NET Core Web API ensures that every request is verified for authenticity and integrity, protecting your application from tampering and unauthorized access. By carefully managing secret keys, validating timestamps and nonces, and integrating a robust middleware, you build a secure communication channel between clients and servers. This not only strengthens your API security but also provides confidence that your sensitive data and operations are safeguarded against common threats, such as replay 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 an ASP.NET Core Web API Application with an Example. I hope you enjoy this article on HMAC Authentication in ASP.NET Core Web API.

1 thought on “HMAC Authentication in ASP.NET Core Web API”

  1. blank

    Want to see HMAC Authentication in action?

    If you prefer learning through video, check out my detailed step-by-step YouTube tutorial on implementing HMAC Authentication in ASP.NET Core Web API! In this video, I walk you through all the key concepts, real-world code, and best practices to secure your Web APIs using HMAC.

    👉 Watch the full video here:
    https://www.youtube.com/watch?v=wfAo8Z5oalA

    If you find the video helpful, please don’t forget to Like, Comment, and Subscribe to our channel for more in-depth .NET tutorials!

Leave a Reply

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