HmacAuth.HttpClient
0.1.1
dotnet add package HmacAuth.HttpClient --version 0.1.1
NuGet\Install-Package HmacAuth.HttpClient -Version 0.1.1
<PackageReference Include="HmacAuth.HttpClient" Version="0.1.1" />
<PackageVersion Include="HmacAuth.HttpClient" Version="0.1.1" />
<PackageReference Include="HmacAuth.HttpClient" />
paket add HmacAuth.HttpClient --version 0.1.1
#r "nuget: HmacAuth.HttpClient, 0.1.1"
#:package HmacAuth.HttpClient@0.1.1
#addin nuget:?package=HmacAuth.HttpClient&version=0.1.1
#tool nuget:?package=HmacAuth.HttpClient&version=0.1.1
HmacAuth
Reusable .NET 8 HMAC authentication components for service-to-service APIs.
License
This project is licensed under the MIT License. See LICENSE.
Projects
src/HmacAuth.CoreShared canonicalization, hashing, and signature primitives.src/HmacAuth.AspNetCoreASP.NET Core authentication handler for verifying inbound HMAC requests.src/HmacAuth.HttpClientDelegatingHandlerfor signing outboundHttpClientrequests.samples/HmacAuth.SampleHostRunnable sample API that verifies signed requests.samples/HmacAuth.SampleCallerRunnable sample API that signs outbound requests to the sample host.tests/HmacAuth.TestsEnd-to-end tests covering success, replay rejection, and expired timestamps.
Usage
This library is intended for service-to-service authentication:
- API A signs outbound requests.
- API B verifies the signature and authenticates API A as a caller.
Both APIs can reference the same solution, but use different packages:
- API A references
HmacAuth.HttpClientandHmacAuth.Core - API B references
HmacAuth.AspNetCoreandHmacAuth.Core
Flow
sequenceDiagram
participant Caller as API A / Caller
participant ClientLib as HmacAuth.HttpClient
participant Host as API B / Host
participant ServerLib as HmacAuth.AspNetCore
participant Store as Credential + Nonce Stores
Caller->>ClientLib: Build outbound request
ClientLib->>ClientLib: Hash body + build canonical request
ClientLib->>ClientLib: Sign with client secret
ClientLib->>Host: Send request with HMAC headers
Host->>ServerLib: Authenticate inbound request
ServerLib->>Store: Resolve client secret and validate nonce
ServerLib->>ServerLib: Recompute body hash + signature
ServerLib-->>Host: Success or Unauthorized
Host-->>Caller: Protected API response
Samples
Two runnable sample apps are included:
samples/HmacAuth.SampleHost: secured API listening onhttp://localhost:5081samples/HmacAuth.SampleCaller: caller API listening onhttp://localhost:5082
Run them in separate terminals:
dotnet run --project samples/HmacAuth.SampleHost --launch-profile http
dotnet run --project samples/HmacAuth.SampleCaller --launch-profile http
Then exercise the flow:
curl http://localhost:5081/public/ping
curl http://localhost:5082/call/whoami
curl -X POST http://localhost:5082/call/echo -H "Content-Type: application/json" -d "{\"message\":\"hello\"}"
The sample apps share these dev-only credentials through their appsettings.json files:
- client id:
sample-caller - secret:
dev-only-secret
API B: verify incoming HMAC requests
appsettings.json:
{
"HmacAuthentication": {
"AllowedClockSkew": "00:05:00",
"RequireNonceValidation": true
}
}
Settings:
AllowedClockSkew: the maximum allowed difference between the request timestamp and the API server clock. Use standardTimeSpanformat such as00:05:00for 5 minutes.RequireNonceValidation: enables replay protection by rejecting reused nonces. Leave this enabled unless you have another replay-prevention mechanism in place.
Program.cs:
using HmacAuth.AspNetCore;
using HmacAuth.Core;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddInMemoryHmacCredentialStore(
[new HmacClientCredentials("client-a", "super-secret-key")]);
builder.Services.AddInMemoryHmacNonceStore();
builder.Services.AddAuthentication(HmacAuthenticationDefaults.AuthenticationScheme)
.AddHmac(builder.Configuration.GetSection("HmacAuthentication"));
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/secure", () => "ok")
.RequireAuthorization(new AuthorizeAttribute
{
AuthenticationSchemes = HmacAuthenticationDefaults.AuthenticationScheme,
});
app.Run();
What this does:
- resolves the caller secret from
IHmacCredentialStore - rejects reused nonces through
IHmacNonceStore - validates the request timestamp window
- validates the body hash
- authenticates the request with scheme
HMAC
If you prefer code-based registration, AddHmac(options => { ... }) still works.
API A: sign outbound requests
using HmacAuth.HttpClient;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("secured-api", client =>
{
client.BaseAddress = new Uri("https://api-b.local/");
})
.AddHmacSigningHandler(options =>
{
options.ClientId = "client-a";
options.Secret = "super-secret-key";
});
Call the protected API:
app.MapGet("/call-api-b", async (IHttpClientFactory httpClientFactory) =>
{
var client = httpClientFactory.CreateClient("secured-api");
var response = await client.GetAsync("/secure");
var body = await response.Content.ReadAsStringAsync();
return Results.Text(body, statusCode: (int)response.StatusCode);
});
The signing handler adds:
Authorization: HMAC {clientId}:{signature}X-Hmac-TimestampX-Hmac-NonceX-Hmac-Content-SHA256
Production wiring
The in-memory stores are only convenient defaults. For real deployments, replace them with your own implementations:
builder.Services.AddSingleton<IHmacCredentialStore, MyCredentialStore>();
builder.Services.AddSingleton<IHmacNonceStore, MyNonceStore>();
Typical production choices:
IHmacCredentialStore: database, configuration-backed client registry, or secret manager lookupIHmacNonceStore: Redis or another shared cache with TTL support
If API B runs on multiple instances, the nonce store should be shared across instances.
Request format
The canonical request currently signs:
- client id
- method
- path
- normalized query string
- timestamp
- nonce
- content hash
That means the client and server must agree on:
- request path
- query-string normalization
- UTF-8 body encoding for the content hash
- the shared client secret
Notes
- The default replay window is 5 minutes.
- Nonce validation is enabled by default.
- Body hashing requires reading the request body; the ASP.NET Core handler buffers the stream and resets it before your endpoint runs.
CI/CD
GitHub Actions workflows are included for:
- CI on every pushed commit, pull requests, and manual runs
- packaging the library projects as NuGet artifacts
- creating a GitHub release and publishing packages to NuGet.org on tags like
v1.0.0 - manual release runs with an explicit version and optional NuGet publish
The release workflow expects a repository secret named NUGET_API_KEY.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- HmacAuth.Core (>= 0.1.1)
- Microsoft.Extensions.Http (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.