Back to: ASP.NET Core Web API Tutorials
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:
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.
With this, our Database with Employees and ClientSecrets tables should be created as shown in the image below:
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:
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:
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.
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!