# Secure API Design in .NET: HMAC Validation, Anti-Replay, and Timestamp Windows

Security in API design often begins and ends with OAuth or JWTs. Those models are robust for user-facing systems, but when services communicate directly, such as partner integrations, webhooks, or internal event forwarding, the guarantees change. You’re no longer authenticating *who* the user is, but *whether the request itself is trustworthy*.

That’s where **HMAC (Hash-Based Message Authentication Code)** comes in. It’s a mechanism for proving that the message content hasn’t been tampered with and that it came from a trusted sender who holds a shared secret. But HMAC on its own is not enough. Without additional layers like **timestamp validation** and **anti-replay protection**, a valid request could be resent indefinitely by an attacker who intercepted it once.

Below, we’ll build a secure API pattern in .NET 8 that uses HMAC verification, timestamp enforcement, and replay protection, a combination that offers robust security for server to server calls and webhook endpoints.

### Understanding HMAC Signatures

An HMAC combines a message and a secret key through a cryptographic hash function (typically SHA-256). The sender computes the hash, includes it in the request (often in a header), and the receiver recomputes it using the same secret. If both hashes match, the message is authentic.

Imagine a simple message:

```csharp
POST /api/ingest
Body: {"amount": 100}
Secret: my_shared_secret
```

The signature is computed like so:

```csharp
HMACSHA256(UTF8("my_shared_secret"), UTF8("POST\n/api/ingest\n{\"amount\":100}"))
```

The resulting hash is a 256-bit digest unique to that combination of input, secret, and algorithm. Any alteration, even a single byte, changes the result entirely.

### Setting up the Middleware

We’ll start by creating a reusable middleware that performs HMAC validation and timestamp checking before any controller logic runs.

#### `HmacValidationMiddleware.cs`

```csharp
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Caching.Memory;

public class HmacValidationOptions
{
    public string SharedSecret { get; set; } = string.Empty;
    public TimeSpan AllowedClockSkew { get; set; } = TimeSpan.FromMinutes(2);
    public TimeSpan ReplayWindow { get; set; } = TimeSpan.FromMinutes(10);
}

public class HmacValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly HmacValidationOptions _options;
    private readonly IMemoryCache _cache;

    public HmacValidationMiddleware(
        RequestDelegate next,
        IOptions<HmacValidationOptions> options,
        IMemoryCache cache)
    {
        _next = next;
        _options = options.Value;
        _cache = cache;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var request = context.Request;
        if (!request.Headers.TryGetValue("X-Signature", out var signatureHeader) ||
            !request.Headers.TryGetValue("X-Timestamp", out var timestampHeader) ||
            !request.Headers.TryGetValue("X-Nonce", out var nonceHeader))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Missing security headers.");
            return;
        }

        if (!DateTimeOffset.TryParse(timestampHeader, out var timestamp))
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
            await context.Response.WriteAsync("Invalid timestamp format.");
            return;
        }

        // Check timestamp freshness
        var now = DateTimeOffset.UtcNow;
        if (timestamp < now - _options.AllowedClockSkew || timestamp > now + _options.AllowedClockSkew)
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Request expired or timestamp invalid.");
            return;
        }

        // Check replay window
        if (_cache.TryGetValue(nonceHeader, out _))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Replay detected.");
            return;
        }

        // Cache nonce to prevent reuse
        _cache.Set(nonceHeader.ToString(), true, _options.ReplayWindow);

        // Rewind body for reading
        request.EnableBuffering();
        using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
        var body = await reader.ReadToEndAsync();
        request.Body.Position = 0;

        var computed = ComputeSignature(request.Method, request.Path, timestampHeader, nonceHeader, body);
        if (!CryptographicOperations.FixedTimeEquals(
                Convert.FromHexString(computed),
                Convert.FromHexString(signatureHeader)))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Signature mismatch.");
            return;
        }

        await _next(context);
    }

    private string ComputeSignature(string method, string path, string timestamp, string nonce, string body)
    {
        var payload = $"{method}\n{path}\n{timestamp}\n{nonce}\n{body}";
        var keyBytes = Encoding.UTF8.GetBytes(_options.SharedSecret);
        using var hmac = new HMACSHA256(keyBytes);
        var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }
}
```

### Registering the Middleware

Add it to your pipeline in `Program.cs`:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<HmacValidationOptions>(builder.Configuration.GetSection("Hmac"));
builder.Services.AddMemoryCache();

var app = builder.Build();

app.UseMiddleware<HmacValidationMiddleware>();
app.MapControllers();

app.Run();
```

Configuration example:

```json
"Hmac": {
  "SharedSecret": "super_secret_shared_key",
  "AllowedClockSkew": "00:02:00",
  "ReplayWindow": "00:10:00"
}
```

---

### Sending Authenticated Requests

On the client side, we generate a new nonce (unique ID) and timestamp for each request, compute the signature, and send them as headers.

#### `HmacClient.cs`

```csharp
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;

public static class HmacClient
{
    public static async Task<HttpResponseMessage> PostAsync(HttpClient client, string url, string body, string secret)
    {
        var timestamp = DateTimeOffset.UtcNow.ToString("O");
        var nonce = Guid.NewGuid().ToString("N");
        var method = "POST";
        var path = new Uri(url).AbsolutePath;

        var signature = ComputeSignature(secret, method, path, timestamp, nonce, body);

        using var content = new StringContent(body, Encoding.UTF8, "application/json");
        var request = new HttpRequestMessage(HttpMethod.Post, url)
        {
            Content = content
        };

        request.Headers.Add("X-Signature", signature);
        request.Headers.Add("X-Timestamp", timestamp);
        request.Headers.Add("X-Nonce", nonce);

        return await client.SendAsync(request);
    }

    private static string ComputeSignature(string secret, string method, string path, string timestamp, string nonce, string body)
    {
        var payload = $"{method}\n{path}\n{timestamp}\n{nonce}\n{body}";
        var keyBytes = Encoding.UTF8.GetBytes(secret);
        using var hmac = new HMACSHA256(keyBytes);
        var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }
}
```

This guarantees that every request carries a fresh, verifiable cryptographic proof tied to its body content.

### Why Fixed Time Comparison Is Good

You might notice the middleware uses `CryptographicOperations.FixedTimeEquals`. This prevents **timing attacks**, subtle differences in comparison time that could leak partial information about the correct signature. Even tiny timing discrepancies, measurable over many requests, can reveal a hash bit by bit. Using a fixed time comparison ensures that the check always takes the same amount of time, regardless of how similar or different the inputs are.

### Preventing Replays with Nonces and Caching

The `X-Nonce` header and memory cache combination stop attackers from reusing a previously valid request. Each nonce (unique per request) is stored temporarily. If it appears again within the replay window, the request is rejected immediately. In production, you might use **Redis** or **Azure Cache for Redis** instead of an in memory cache to allow distributed verification across multiple instances.

```csharp
builder.Services.AddStackExchangeRedisCache(o =>
{
    o.Configuration = builder.Configuration.GetConnectionString("Redis");
});
```

Replace `_cache` usage with Redis operations to persist replay detection across the cluster.

### Enforcing Timestamp Windows

Attackers can sometimes delay or reorder requests. The timestamp ensures each request is only valid for a brief time. The middleware’s check:

```csharp
if (timestamp < now - AllowedClockSkew || timestamp > now + AllowedClockSkew)
```

allows a ±2-minute drift between client and server clocks, rejecting requests outside that range. Combined with nonce caching, this closes almost all replay vectors.

### Extending for Multi Key Clients

In real systems, different clients (vendors, webhooks, or internal services) often have their own secrets. Extend the middleware to load a secret dynamically based on a `X-ClientId` header.

```csharp
if (!context.Request.Headers.TryGetValue("X-ClientId", out var clientId))
{
    context.Response.StatusCode = 400;
    await context.Response.WriteAsync("Missing client ID.");
    return;
}

var secret = _clientSecretStore.GetSecret(clientId);
if (secret is null)
{
    context.Response.StatusCode = 401;
    await context.Response.WriteAsync("Unknown client ID.");
    return;
}
```

This allows you to rotate or revoke credentials per client, without redeploying.

### Combining with HTTPS and Additional Layers

HMAC does not replace HTTPS, it complements it. HTTPS encrypts the transport channel, while HMAC validates message integrity and authenticity. Always enforce TLS 1.2+ for such APIs.

Further enhancements might include:

* **Payload compression detection** to ensure clients don’t compress then hash different representations.
    
* **Canonicalisation** to normalise whitespace or header order before hashing.
    
* **Versioned signature formats** (e.g. `v1:` prefix) to allow future cryptographic changes without breaking existing clients.
    
* ---
    

HMAC validation, timestamp enforcement, and replay protection form a triad that hardens server to server APIs against tampering, forgery, and replay attacks. In this post, we’ve built a complete example in .NET 8 using middleware, fixed time comparison, nonce caching, and timestamp checks, all lightweight, dependency free techniques. In distributed systems, cryptographic verification is all about confidence. The ability to prove a request’s integrity is as fundamental as authenticating who sent it. Whether you’re securing webhook receivers, internal APIs, or financial integrations, adopting this pattern ensures every request can be trusted, every time.
