0% found this document useful (0 votes)
99 views

Part - 2 HMAC Authentication in ASP - NET Core Web API

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
99 views

Part - 2 HMAC Authentication in ASP - NET Core Web API

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 12

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 which is used for creating a Message Authentication Code by using a Hash
Function.

The most important thing that you need to keep in mind is that while generating the Message
Authentication Code using 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.

How HMAC Authentication Works?


HMAC ensures that the data has not been tampered with during transmission by creating a unique
hash for each message based on its content and a secret key. This hash is then sent along with the
message. Upon receipt, the receiver can generate their 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:

What are the components involved in HMAC Authentication?


HMAC Authentication in ASP.NET Core Web API involves several key components that work together
to ensure the integrity and authenticity of messages. The following are the main components involved
in HMAC Authentication:
 Message: The message is the data that needs to be authenticated. In the context of an API,
this typically includes parts of the HTTP request such as the HTTP Method, URL, Route Data,
Query Parameters, Headers, and Body of the request in case of POST or PUT Request.
 Secret Key: The secret key is a shared secret between the client and the server. It is used in
the HMAC Algorithm to create a hash that is unique to the message. This key must be kept
confidential and secure.
 Hash Function: The hash function is a cryptographic algorithm used to generate the hash.
Common hash functions used in HMAC include SHA-256, SHA-512, and MD5.
 HMAC Algorithm: The HMAC (Hash-based Message Authentication Code) algorithm
combines the message and the secret key using the hash function to produce a unique hash
code.
 Timestamp: A timestamp is often included in the message to prevent replay attacks. It
ensures that the message is valid only for a certain period, making it difficult for attackers to
reuse captured messages.
 Nonce (Optional): A nonce is a unique value added to the message to further prevent replay
attacks. It ensures that each message has a unique HMAC even if the other components are
the same.
 Authorization Header: The resulting HMAC is included in the HTTP request's Authorization
header. This header might look like this: Authorization: HMAC <computed-hmac>

Implementing HMAC in ASP.NET Core Web API


Let us first understand the implementation steps of HMAC Authentication in ASP.NET Core Web API:
 Create Middleware: Implement a Middleware component to handle the HMAC authentication
logic. This middleware will extract the HMAC from the request, reconstruct the message,
compute the HMAC, and compare it with the received HMAC.
 Register Middleware: Register HMAC middleware Component to the Request Processing
Pipeline (within the Main method of the Program class) to ensure it runs on every request.
 Client-Side HMAC Generation: Ensure that the client applications generate the HMAC using
the same method, same logic and using the same secret key, and include it in the request
headers.
 Send Secure Requests: Include necessary headers such as Authorization in the requests
which store the HMAC authentication signature.

Creating the Server Application:


Let us first create the server application where we will validate the HMAC Authentication. Create a
new ASP.NET Core Web API application named HMACServerApp.

Managing the Client Secret:


To manage different secret keys for different clients (Mobile, Desktop, Web), 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.

For simplicity, let us use a dictionary to store the client IDs and their corresponding secret keys. In a
real-world application, this would be a database or a secure key management service. Let us assume
our services are going to be consumed by three different types of clients: Client1, Client2, and Client3.
So, create a class file named ClientSecrets.cs and then copy and paste the following code:
namespace HMACServerApp.Models
{
public static class ClientSecrets
{
public static Dictionary<string, string> Secrets = new Dictionary<string, string>
{
{ "ClientId1", "SecretKey1" },
{ "ClientId2", "SecretKey2" },
{ "ClientId3", "SecretKey3" },
// Add more clients as needed
};
}
}

Middleware for HMAC Authentication in ASP.NET Core Web API:


Let us create a custom middleware component to handle the extraction and validation of the HMAC
from incoming requests. So, create a class file named HMACAuthenticationMiddleware.cs and then
copy and paste the following code. The following code is self-explained, so please go through 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;

// Nonce expiry time to prevent reuse of nonces


private static readonly TimeSpan NonceExpiry = TimeSpan.FromMinutes(5);

// Timestamp tolerance to allow slight time differences between client and server
private static readonly TimeSpan TimestampTolerance = TimeSpan.FromMinutes(5);

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

// Main method to handle each request and perform HMAC validation


public async Task Invoke(HttpContext context)
{
// 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

// Validate the client ID and get the secret key


if (!ClientSecrets.Secrets.TryGetValue(clientId, out var secretKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid client ID");
return;
}

// Validate the timestamp to prevent replay attacks


if (!DateTimeOffset.TryParse(timestamp, out var requestTime) ||
Math.Abs((DateTimeOffset.UtcNow - requestTime).TotalMinutes) >
TimestampTolerance.TotalMinutes)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid or expired timestamp");
return;
}

// Add the nonce to the cache to prevent reuse


if (!AddNonce(nonce))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Nonce already used");
return;
}

// 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 add a nonce to the cache


private bool AddNonce(string nonce)
{
if (_memoryCache.TryGetValue(nonce, out _))
{
return false; // Nonce already used
}

var cacheEntryOptions = new


MemoryCacheEntryOptions().SetAbsoluteExpiration(NonceExpiry);
_memoryCache.Set(nonce, true, cacheEntryOptions);
return true;
}

// 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 hash


using var hmac = new HMACSHA256(secretBytes);
var computedHash = hmac.ComputeHash(requestBytes);
var computedToken = Convert.ToBase64String(computedHash);
return token == computedToken;
}
}
}

Understanding the HMAC Middleware Components in ASP.NET Core Web API


RequestDelegate: This delegate represents the next middleware in the pipeline. It allows the current
middleware to call the next middleware after it has completed its own processing.

IMemoryCache: This is used to store nonces temporarily in memory to prevent replay attacks. The
cache helps in keeping track of used nonces and their expiry.
NonceExpiry: Defines the time duration for which a nonce is considered valid. After this time, the
nonce is expired and removed from the cache.

TimestampTolerance: Specifies the allowable time difference between the client's timestamp and the
server's current time to account for slight variations in clock times. This helps in preventing replay
attacks.

Constructor: Initializes the middleware with the next request delegate and memory cache.

Invoke Method:
This is the core method that gets called for each HTTP request. It performs the following tasks:
 Validates the presence and format of the Authorization header.
 Extracts and validates the client ID, HMAC token, nonce, and timestamp.
 Checks for nonce reuse and timestamp validity.
 Reads the request body for POST and PUT methods.
 Validates the HMAC token.
 Calls the next middleware in the pipeline if the HMAC token is valid.

AddNonce Method: Adds a nonce to the cache to prevent reuse. Returns false if the nonce is
already used, indicating a potential replay attack.

ValidateToken Method: Constructs the request content string, including the HTTP method, path,
nonce, timestamp, and request body (if applicable). Computes the HMAC hash using the secret key
and compares it with the token provided in the request.

Why is EnableBuffering Needed?


When an HTTP request with a body (such as a POST or PUT request) is received, the request body is
typically a stream that can only be read once. After reading the body, the stream position is at the end,
and subsequent reads will return nothing unless the stream is reset. By calling EnableBuffering(), we
enable multiple reads of the request body.

Why Resetting the Stream Position is Necessary?


When we read the request body in our middleware to compute the HMAC for validation, the stream's
position moves to the end of the stream. If the position is not reset, any subsequent attempts to read
the stream (for example, by downstream middleware or the controller handling the request) will return
no data because the stream has already been read.

Registering Memory Cache and HMAC Middleware Components:


Register the IMemoryCache service to the built-in dependency injection container and HMAC
Middleware Components to the Application Request Processing Pipeline. So, please modify the
Program class as follows:
using HMACServerApp.Models;
namespace HMACServerApp
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

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

//Adding In-Memory Caching


builder.Services.AddMemoryCache();

var app = builder.Build();

// Configure the HTTP request pipeline.


if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

//Registering the HmacAuthenticationMiddleware


app.UseMiddleware<HMACAuthenticationMiddleware>();

app.MapControllers();

app.Run();
}
}
}

Creating Employee Model:


Let us create the Employee model using which we will perform the Database CRUD Operations. So,
add a class file named Employee.cs and then copy and paste the following code:
namespace HMACServerApp.Models
{
public class Employee
{
public int? Id { get; set; }
public string Name { get; set; }
public string Position { get; set; }
public decimal Salary { get; set; }
}
}

Create Employees Controller


Next, we need to create the Employees Controller which will expose the end points 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;

namespace HMACServerApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
private static List<Employee> _employees = new List<Employee>
{
new Employee { Id = 1, Name = "Alice Smith", Position = "Manager", Salary = 75000 },
new Employee { Id = 2, Name = "Bob Johnson", Position = "Developer", Salary =
60000 },
new Employee { Id = 3, Name = "Carol White", Position = "Designer", Salary = 55000 }
};
// GET: api/Employees
[HttpGet]
public ActionResult<IEnumerable<Employee>> GetEmployees()
{
return _employees;
}

// GET: api/Employees/5
[HttpGet("{id}")]
public ActionResult<Employee> GetEmployee(int id)
{
var employee = _employees.FirstOrDefault(e => e.Id == id);

if (employee == null)
{
return NotFound();
}

return employee;
}

// POST: api/Employees
[HttpPost]
public ActionResult<Employee> PostEmployee(Employee employee)
{
employee.Id = _employees.Max(e => e.Id) + 1;
_employees.Add(employee);

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


}

// PUT: api/Employees/5
[HttpPut("{id}")]
public IActionResult PutEmployee(int id, Employee employee)
{
var existingEmployee = _employees.FirstOrDefault(e => e.Id == id);
if (existingEmployee == null)
{
return NotFound();
}

existingEmployee.Name = employee.Name;
existingEmployee.Position = employee.Position;
existingEmployee.Salary = employee.Salary;

return NoContent();
}

// DELETE: api/Employees/5
[HttpDelete("{id}")]
public IActionResult DeleteEmployee(int id)
{
var employee = _employees.FirstOrDefault(e => e.Id == id);
if (employee == null)
{
return NotFound();
}

_employees.Remove(employee);
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 are going to create a Console Application. So, create a .NET Core Console
Application named HMACClientApp.

Creating HMAC Helper Class:


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 method called GenerateHmacToken which 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.
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 in ISO 8601 format


var timestamp = DateTime.UtcNow.ToString("o");

// 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 is used for generating 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 Main method of the Program class as follows. Here, we are consuming the API
services by calling the respective end points. Also, we are passing the HMAC token using the
Authorization header.
using System.Text;
namespace HMACClientApp
{
internal class Program
{
static async Task Main(string[] args)
{
//You need to use proper Client Id, Secret and Base URL of the API
var clientId = "ClientId1";
var secretKey = "SecretKey1";
var baseUrl = "https://localhost:7010/api/Employees";

using var client = new HttpClient();

// Create an Employee
var employee = new
{
Name = "Pranaya Rout",
Position = "Developer",
Salary = 60000
};

var requestBody = System.Text.Json.JsonSerializer.Serialize(employee);


var token = HMACHelper.GenerateHmacToken("POST", "/api/employees", clientId,
secretKey, requestBody);

var requestMessage = new HttpRequestMessage(HttpMethod.Post, baseUrl)


{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};
requestMessage.Headers.Add("Authorization", $"HMAC {token}");

var response = await client.SendAsync(requestMessage);


var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"POST Response: {responseContent}");

// Get all Employees


token = HMACHelper.GenerateHmacToken("GET", "/api/employees", clientId,
secretKey);

requestMessage = new HttpRequestMessage(HttpMethod.Get, baseUrl);


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

response = await client.SendAsync(requestMessage);


responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"GET Response: {responseContent}");

// Assuming the created employee has an ID of 1 (adjust as necessary)


// Get Employee by ID
var employeeId = 1;
var getByIdUrl = $"{baseUrl}/{employeeId}";

token = HMACHelper.GenerateHmacToken("GET", $"/api/employees/{employeeId}",


clientId, secretKey);

requestMessage = new HttpRequestMessage(HttpMethod.Get, getByIdUrl);


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

response = await client.SendAsync(requestMessage);


responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"GET by ID Response: {responseContent}");

// Update Employee
var updatedEmployee = new
{
Id = employeeId,
Name = "Rakesh Sharma",
Position = "Senior Developer",
Salary = 80000
};
requestBody = System.Text.Json.JsonSerializer.Serialize(updatedEmployee);
token = HMACHelper.GenerateHmacToken("PUT", $"/api/employees/{employeeId}",
clientId, secretKey, requestBody);

requestMessage = new HttpRequestMessage(HttpMethod.Put, getByIdUrl)


{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};
requestMessage.Headers.Add("Authorization", $"HMAC {token}");

response = await client.SendAsync(requestMessage);


responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"PUT Response: {responseContent}");

// Delete Employee
token = HMACHelper.GenerateHmacToken("DELETE", $"/api/employees/{employeeId}",
clientId, secretKey);

requestMessage = new HttpRequestMessage(HttpMethod.Delete, getByIdUrl);


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

response = await client.SendAsync(requestMessage);


responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"DELETE Response: {responseContent}");

Console.ReadKey();
}
}
}

With the above changes in place, first run the Server application and then run the client application
and you should see it is working as expected. If everything is work as expected, then you will see the
following output:
What is Replay Attack in HMAC Authentication?
A replay attack is a form of 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 Replay Attacks Work?


 Interception: An attacker captures a legitimate HTTP request that includes the HMAC
signature and all the relevant data (like API keys, timestamps, and the request body).
 Replay: The attacker sends this intercepted request to the server one or more times.
 Processing: If the server does not properly verify the uniqueness or timeliness of each
request, it processes the replayed request just as it would a new, legitimate request.

Preventing Replay Attacks


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
that 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 that the client includes in each
request. The server keeps track of all used nonces to ensure that each request is processed
only once.

When to Use HMAC in ASP.NET Core Web API?


HMAC is useful 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.

You might also like