A HMAC (Hash-based Message Authentication Code) authentication system for ASP.NET Core applications. This library provides both server-side authentication and client-side HTTP handlers for secure API communication.
| Package | Version | Description |
|---|---|---|
| HashGate.AspNetCore | Server-side HMAC authentication for ASP.NET Core applications | |
| HashGate.HttpClient | Client-side HTTP message handler for HMAC authentication |
- Secure HMAC-SHA256 authentication with timestamp validation
- Easy integration with ASP.NET Core authentication system
- Client library included for .NET HttpClient integration
- Request replay protection with configurable time windows and optional signature replay cache
- Nonce support for guaranteed per-request signature uniqueness
- Highly configurable key providers and validation options
This library implements HMAC authentication similar to AWS Signature Version 4 and Azure HMAC Authentication. All HTTP requests must be transmitted over TLS and include cryptographic signatures to ensure request integrity and authenticity.
HMAC authentication is particularly well-suited for server-to-server communication and microservices architectures for following reasons:
- No credentials in transit: Unlike bearer tokens, HMAC signatures are computed from request data, meaning the actual secret never travels over the network
- Request integrity: Each request is cryptographically signed, ensuring the payload hasn't been tampered with during transmission
- Replay attack protection: Built-in timestamp validation prevents malicious replaying of captured requests
- Stateless authentication: No need for centralized token stores or session management across services
- Service-to-service isolation: Each service can have unique HMAC keys, limiting blast radius if one service is compromised
- Zero-dependency authentication: No reliance on external identity providers or token validation services
- High performance: HMAC computation is fast and doesn't require network calls to validate authenticity
- Reduced infrastructure: No need for token refresh endpoints, session stores, or identity service dependencies
- Deterministic debugging: Failed requests can be reproduced locally since signatures are deterministic
- Language agnostic: HMAC-SHA256 is supported by virtually every programming language and platform
- Framework independent: Works with any HTTP client/server combination, not tied to specific OAuth flows
- Custom key management: Full control over key rotation, storage, and distribution strategies
- No single point of failure: Authentication doesn't depend on external services being available
- Linear scaling: Authentication overhead doesn't increase with the number of services or requests
- Offline capability: Services can authenticate requests even when disconnected from identity providers
- Internal API gateways communicating with backend services
- Microservice mesh where services need to authenticate each other
- Webhook validation from external systems
- Background job services accessing protected APIs
- IoT device communication where OAuth flows are impractical
Install the NuGet packages for your server and client projects:
Using .NET CLI:
dotnet add package HashGate.AspNetCoreUsing PowerShell:
Install-Package HashGate.AspNetCoreUsing .NET CLI:
dotnet add package HashGate.HttpClientUsing PowerShell:
Install-Package HashGate.HttpClientusing HashGate.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add HMAC authentication
builder.Services
.AddAuthentication()
.AddHmacAuthentication();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Your protected endpoints
app.MapGet("/api/secure", () => "Hello, authenticated user!")
.RequireAuthorization();
app.Run();appsettings.json (Server):
{
"HmacSecrets": {
"MyClientId": "your-secret-key-here",
"AnotherClient": "another-secret-key"
}
}using HashGate.HttpClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Add HMAC authentication services
builder.Services.AddHmacAuthentication();
// Configure HttpClient with HMAC authentication
builder.Services
.AddHttpClient("SecureApi", client => client.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<HmacAuthenticationHttpHandler>();
var app = builder.Build();
// Get the HttpClient and make authenticated requests
var httpClientFactory = app.Services.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("SecureApi");
var response = await httpClient.GetAsync("/api/secure");appsettings.json (Client):
{
"HmacAuthentication": {
"Client": "MyClientId",
"Secret": "your-secret-key-here"
}
}Each client must have:
- Client - A unique identifier for the access key used to compute the signature
- Secret - The secret key used for HMAC-SHA256 signature generation
Every authenticated request must include these headers:
| Request Header | Description |
|---|---|
| Host | Internet host and port number |
| x-timestamp | Unix timestamp (seconds since epoch) when the request was created. Must be within 5 minutes of current server time |
| x-content-sha256 | Base64-encoded SHA256 hash of the request body. Required even for requests with empty bodies |
| x-nonce | Optional unique per-request value (GUID). Recommended when replay protection is enabled (the default) |
| Authorization | HMAC authentication information (see format details below) |
GET /api/users?page=1 HTTP/1.1
Host: api.example.com
x-timestamp: 1722776096
x-nonce: a3f1c2d4e5b64a7f8c9d0e1f2a3b4c5d
x-content-sha256: 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
Authorization: HMAC Client=123456789&SignedHeaders=host;x-timestamp;x-content-sha256;x-nonce&Signature=AbCdEf123456...Authorization: HMAC Client=<value>&SignedHeaders=<value>&Signature=<value>
| Parameter | Description | Required |
|---|---|---|
| HMAC | Authorization scheme identifier | Yes |
| Client | The client identifier of the access key used to compute the signature | Yes |
| SignedHeaders | Semicolon-separated list of HTTP headers included in the signature | Yes |
| Signature | Base64-encoded HMACSHA256 hash of the String-To-Sign | Yes |
The unique identifier for the access key used to compute the signature. This allows the server to identify which HMAC secret key to use for signature verification.
Semicolon-separated list of HTTP header names that were included in the signature calculation. These headers must be present in the request with the exact values used during signing.
The following headers must always be included in SignedHeaders. The server enforces their presence and will reject requests that omit them:
hostx-timestamp— required for replay protection; cryptographically binds the timestamp to the signaturex-content-sha256— required for body integrity; cryptographically binds the content hash to the signature
You can include additional headers beyond the required set for enhanced security:
host;x-timestamp;x-content-sha256;content-type;accept
The signature is a Base64-encoded HMACSHA256 hash of the String-To-Sign using the client's secret key:
Signature = base64_encode(HMACSHA256(String-To-Sign, Secret))
The String-To-Sign is a canonical representation of the request constructed as follows:
String-To-Sign = HTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_values
| Component | Description |
|---|---|
| HTTP_METHOD | Uppercase HTTP method name (GET, POST, PUT, DELETE, etc.) |
| path_and_query | The request path and query string (e.g., /api/users?page=1) |
| signed_headers_values | Semicolon-separated list of header values in the same order as specified in SignedHeaders |
For a GET request to /kv?fields=*&api-version=1.0:
String-To-Sign =
"GET" + '\n' + // HTTP method
"/kv?fields=*&api-version=1.0" + '\n' + // path and query
"api.example.com;1722776096;47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" // header values// Basic configuration with default settings
builder.Services
.AddAuthentication()
.AddHmacAuthentication();
// Configuration with custom options
builder.Services
.AddAuthentication()
.AddHmacAuthentication(options =>
{
options.ToleranceWindow = 10; // 10 minutes timestamp tolerance
options.SecretSectionName = "MyHmacSecrets"; // Custom config section
});HashGate provides two layers of replay protection:
Layer 1 — Timestamp window (always active): The server rejects any request whose x-timestamp falls outside ToleranceWindow minutes of server time (default: 5 minutes).
Layer 2 — Signature replay cache (enabled by default): EnableReplayProtection is true by default. The server records each validated signature and immediately rejects any duplicate that arrives within its validity window — even within the same second. To opt out, set EnableReplayProtection = false.
// Server — replay protection is enabled by default
builder.Services
.AddAuthentication()
.AddHmacAuthentication();
// To disable replay protection:
// .AddHmacAuthentication(options => options.EnableReplayProtection = false);Important — nonce: The timestamp has only one-second resolution, so two identical requests sent within the same second produce the same signature and the second would be falsely rejected. Every request automatically includes a unique
x-nonceheader, making each signature cryptographically unique.
// Client — x-nonce is always included automatically
services.AddHmacAuthentication(options =>
{
options.Client = "MyClientId";
options.Secret = "my-secret-key";
});Multi-server / distributed deployments: The default DefaultHmacReplayProtection is backed by HybridCache (in-process L1). Register a distributed cache (e.g. Redis) alongside it; HybridCache will automatically promote it to the L2 backing store — no custom code required.
// Add Redis as the distributed backing store for replay protection
builder.Services.AddStackExchangeRedisCache(options =>
options.Configuration = builder.Configuration.GetConnectionString("Redis"));
builder.Services
.AddAuthentication()
.AddHmacAuthentication();// Basic configuration using appsettings.json
services.AddHmacAuthentication();
// Configuration with custom options
services.AddHmacAuthentication(options =>
{
options.Client = "MyClientId";
options.Secret = "my-secret-key";
options.SignedHeaders = ["host", "x-timestamp", "x-content-sha256", "content-type"];
});Implement IHmacKeyProvider to load keys from your preferred storage:
public class DatabaseKeyProvider : IHmacKeyProvider
{
private readonly IKeyRepository _keyRepository;
public DatabaseKeyProvider(IKeyRepository keyRepository)
{
_keyRepository = keyRepository;
}
public async ValueTask<string?> GetSecretAsync(string client, CancellationToken cancellationToken = default)
{
var key = await _keyRepository.GetKeyAsync(client, cancellationToken);
return key?.Secret;
}
public async ValueTask<ClaimsIdentity> GenerateClaimsAsync(string client, string? scheme = null, CancellationToken cancellationToken = default)
{
var identity = new ClaimsIdentity(scheme);
identity.AddClaim(new Claim(ClaimTypes.Name, client));
// Add additional claims based on your requirements
var model = await _keyRepository.GetClientAsync(client, cancellationToken);
if (model != null)
{
identity.AddClaim(new Claim("display_name", model.DisplayName));
// Add role claims, permissions, etc. as needed
}
return identity;
}
}
// Register in DI container with custom key provider
builder.Services
.AddAuthentication()
.AddHmacAuthentication<DatabaseKeyProvider>();
// Or register the key provider separately if needed
builder.Services.AddScoped<IHmacKeyProvider, DatabaseKeyProvider>();
builder.Services.AddScoped<IKeyRepository, KeyRepository>();The repository includes comprehensive sample implementations:
ASP.NET Core minimal API demonstrating server-side HMAC authentication:
- Location:
samples/Sample.MinimalApi/ - Features: Protected endpoints, OpenAPI integration, custom key provider
- Run:
dotnet run --project samples/Sample.MinimalApi
.NET client implementation using HttpClient with HMAC authentication:
- Location:
samples/Sample.Client/ - Features: Automatic signature generation, HttpClient integration, background service
- Run:
dotnet run --project samples/Sample.Client
Bruno API collection demonstrating HMAC authentication:
- Location:
samples/Sample.Bruno/ - Features: Pre-request HMAC authentication script, public and authenticated endpoints, environment configuration
- Requirements: Bruno in Developer Mode for Node.js module support
- Usage: Import the collection into Bruno and test endpoints
JavaScript/Node.js client implementation:
- Location:
samples/Sample.JavaScript/ - Features: Browser and Node.js compatible, TypeScript definitions
- Run:
npm install && npm start
Python client implementation:
- Location:
samples/Sample.Python/ - Features: Easy-to-use client class, demo script, interactive testing tool, unit tests
- Requirements: Python 3, dependencies in
requirements.txt - Run:
pip install -r requirements.txt && python demo.py
Java client implementation using the built-in java.net.http.HttpClient:
- Location:
samples/Sample.Java/ - Features: HMAC client class, demo and example apps, unit tests, no external HTTP dependencies
- Requirements: Java 25+, Maven 3.9+
- Run:
mvn compile && mvn exec:java
- Always use HTTPS in production environments
- Protect HMAC secret keys - never expose them in client-side code
- Always include required signed headers -
x-timestampandx-content-sha256must be inSignedHeaders(the server enforces this) - Monitor timestamp tolerance - shorter windows provide better security
- Rotate keys regularly - implement key rotation policies
- Log authentication failures - monitor for potential attacks
- Validate all inputs - especially timestamp and signature formats
-
"Invalid signature" errors:
- Verify client and server are using the same secret key
- Check that all required headers are included and properly formatted
- Ensure timestamp is within the allowed window
-
"Timestamp validation failed":
- Synchronize client and server clocks
- Adjust
ToleranceWindowif needed
-
"Missing required headers":
- Ensure
host,x-timestamp, andx-content-sha256headers are present
- Ensure
-
"Replayed signature" errors when
EnableReplayProtectionis enabled:- Every request automatically includes a unique
x-nonceheader, ensuring every signature is unique
- Every request automatically includes a unique
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Version 3 introduces nonce support to guarantee per-request signature uniqueness. The x-nonce header is now included in the default signed headers.
Note: The
x-nonceheader is not required by the server. However, without it, two identical requests sent within the same second produce the same signature. BecauseEnableReplayProtectionis enabled by default, the second request will be rejected with a 401 Unauthorized response because the signature was already recorded. Includingx-nonceis strongly recommended to avoid this.
| Area | v2.x | v3.x |
|---|---|---|
| Default signed headers | host;x-timestamp;x-content-sha256 |
host;x-timestamp;x-content-sha256;x-nonce |
x-nonce header |
Not present | Optional but included by default; automatically generated (GUID) on every request |
| Replay protection | Identical requests in the same second produced the same signature | Each request has a cryptographically unique signature via nonce |
If you are using the default signed headers, no code changes are required. The client automatically generates and includes x-nonce on every request.
If you have custom SignedHeaders configured, add x-nonce to the list:
services.AddHmacAuthentication(options =>
{
options.Client = "MyClientId";
options.Secret = "my-secret-key";
- options.SignedHeaders = ["host", "x-timestamp", "x-content-sha256", "content-type"];
+ options.SignedHeaders = ["host", "x-timestamp", "x-content-sha256", "x-nonce", "content-type"];
});Update the NuGet package to v3.x. The server automatically recognizes x-nonce in the SignedHeaders list. No configuration changes are needed unless you have custom header validation logic.
All non-.NET clients should be updated to include x-nonce. While the server does not require it, omitting the nonce means identical requests within the same second will share the same signature and be rejected with 401 because EnableReplayProtection is enabled by default.
- Generate a unique nonce (GUID/UUID) for each request
- Set the
x-nonceheader on the request - Add
x-nonceto theSignedHeaderslist in theAuthorizationheader so the nonce value is included in the string-to-sign and the resulting signature
This implementation is inspired by: