HmacAuth.HttpClient 0.1.1

dotnet add package HmacAuth.HttpClient --version 0.1.1
                    
NuGet\Install-Package HmacAuth.HttpClient -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HmacAuth.HttpClient" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HmacAuth.HttpClient" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="HmacAuth.HttpClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HmacAuth.HttpClient --version 0.1.1
                    
#r "nuget: HmacAuth.HttpClient, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HmacAuth.HttpClient@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HmacAuth.HttpClient&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=HmacAuth.HttpClient&version=0.1.1
                    
Install as a Cake Tool

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.Core Shared canonicalization, hashing, and signature primitives.
  • src/HmacAuth.AspNetCore ASP.NET Core authentication handler for verifying inbound HMAC requests.
  • src/HmacAuth.HttpClient DelegatingHandler for signing outbound HttpClient requests.
  • samples/HmacAuth.SampleHost Runnable sample API that verifies signed requests.
  • samples/HmacAuth.SampleCaller Runnable sample API that signs outbound requests to the sample host.
  • tests/HmacAuth.Tests End-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.HttpClient and HmacAuth.Core
  • API B references HmacAuth.AspNetCore and HmacAuth.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 on http://localhost:5081
  • samples/HmacAuth.SampleCaller: caller API listening on http://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 standard TimeSpan format such as 00:05:00 for 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-Timestamp
  • X-Hmac-Nonce
  • X-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 lookup
  • IHmacNonceStore: 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:

  1. client id
  2. method
  3. path
  4. normalized query string
  5. timestamp
  6. nonce
  7. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 107 3/12/2026
0.1.0 111 3/12/2026