ACYTEC.Security.Hmac.AspNetCore.Functions 1.0.0

dotnet add package ACYTEC.Security.Hmac.AspNetCore.Functions --version 1.0.0
                    
NuGet\Install-Package ACYTEC.Security.Hmac.AspNetCore.Functions -Version 1.0.0
                    
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="ACYTEC.Security.Hmac.AspNetCore.Functions" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ACYTEC.Security.Hmac.AspNetCore.Functions" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="ACYTEC.Security.Hmac.AspNetCore.Functions" />
                    
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 ACYTEC.Security.Hmac.AspNetCore.Functions --version 1.0.0
                    
#r "nuget: ACYTEC.Security.Hmac.AspNetCore.Functions, 1.0.0"
                    
#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 ACYTEC.Security.Hmac.AspNetCore.Functions@1.0.0
                    
#: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=ACYTEC.Security.Hmac.AspNetCore.Functions&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=ACYTEC.Security.Hmac.AspNetCore.Functions&version=1.0.0
                    
Install as a Cake Tool

ACYTEC.Security.Hmac.AspNetCore.Functions

Azure Functions isolated-worker adapter for ACYTEC.Security.Hmac.AspNetCore. Adds the same HMAC signing, JWT auth, origin validation, and AES-GCM payload encryption to a Functions app that the base package gives an ASP.NET Core Web API, without pulling Azure Functions Worker dependencies into the base package for consumers who don't need them.

Why this is a separate package: Azure Functions isolated worker apps don't have a WebApplication/IApplicationBuilder, which is what the base package's UseACYTECHmacSecurity() requires. Even with the ASP.NET Core integration extension (Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore) installed, FunctionsApplication.CreateBuilder(args).Build() returns a plain IHost, never a WebApplication. This package reuses every HttpContext based middleware class from the base package unchanged, composed by hand into the Functions worker's own middleware pipeline instead.

Install

dotnet add package ACYTEC.Security.Hmac.AspNetCore.Functions

Your Functions project also needs Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore (this package depends on it, but Functions projects typically reference it directly too; HttpContext binding only works with it installed).

Quick start

// Program.cs
using ACYTEC.Security.Hmac.AspNetCore.Extensions;         // AddACYTECHmacSecurity, from the base package
using ACYTEC.Security.Hmac.AspNetCore.Functions.Extensions; // UseACYTECHmacSecurity, from this package

var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

builder.Services.AddACYTECHmacSecurity(builder.Configuration);
builder.UseACYTECHmacSecurity();

builder.Build().Run();

The token endpoint, POST /auth/token, is a [Function] shipped inside this package. It's discovered automatically by the Functions build the same way any function in a referenced project or package is, nothing to write for it.

Writing a protected function

Bind the trigger as HttpRequestData, not HttpContext, and take FunctionContext as a second parameter. Fetch the real request from FunctionContext.GetHttpContext() inside the body:

using ACYTEC.Security.Hmac.AspNetCore.Functions.Authorization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

public sealed class SearchUsersFunction
{
    [Function("SearchUsers")]
    [RequireACYTECScope("UserApi.Search")]
    public async Task Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "users/search")] HttpRequestData _,
        FunctionContext functionContext)
    {
        var context = functionContext.GetHttpContext()!;
        var query = context.Request.Query["query"].ToString();
        await context.Response.WriteAsJsonAsync(new { query, results = Array.Empty<string>() });
    }
}

Binding HttpContext directly as the trigger parameter type comes back null. This was verified against a real running Functions host, not just assumed: ACYTECHmacFunctionsMiddleware runs ahead of the ASP.NET Core integration's own request proxying middleware, and that combination reliably produced a null HttpContext for the trigger parameter, with or without a second FunctionContext parameter. Always use the HttpRequestData + FunctionContext.GetHttpContext() pattern above for any new function.

Existing functions written against HttpRequest (classic ASP.NET Core MVC style, returning IActionResult) are unaffected and don't need to change. This was also verified live, against a real production Functions app's existing, unmodified HttpRequest req / Task<IActionResult> endpoints: ACYTECHmacFunctionsMiddleware authenticated the bearer token, validated origin, and invoked them exactly as before. It's specifically the HttpContext type as the bound parameter that comes back null; HttpRequest and IActionResult go through a different binding path that isn't affected.

[HttpTrigger(AuthorizationLevel.Anonymous, ...)] is correct, not a mistake: authentication and authorization are handled by ACYTECHmacFunctionsMiddleware (bearer token, origin, scope), not by Functions' own key based AuthorizationLevel mechanism.

Scope requirements

Azure Functions has no fluent endpoint metadata like ASP.NET Core's RequireAuthorization(policy => ...), since there's no WebApplication route builder to attach one to. [RequireACYTECScope("Scope.A", "Scope.B")] on the function method is the equivalent: the caller's token must carry at least one of the listed scopes. Omit the attribute for a function that only needs a valid token, no specific scope.

Enabling or disabling payload encryption

Same single switch as the base package, ACYTECHmacSecurity:EnablePayloadEncryption (default true), bound the same way via AddACYTECHmacSecurity. Setting it to false covers TokenFunction's response too, it returns plain { "accessToken": "...", "expiresAt": "..." } instead of an encrypted envelope, not just the middleware's request/response encryption for other functions. Useful for local testing with a tool that can't do AES-GCM, see Postman below.

Testing with Postman

Postman's pre-request script sandbox bundles CryptoJS, which handles the HMAC-SHA256 signing fine, but not AES-GCM, CryptoJS doesn't include GCM mode. Set EnablePayloadEncryption to false in local.settings.json while testing in Postman:

{
  "Values": {
    "ACYTECHmacSecurity:EnablePayloadEncryption": "false"
  }
}

Then, environment variables baseUrl (e.g. http://localhost:7071), tokenPath (e.g. /api/auth/token, the Functions default route prefix api plus the fixed auth/token route), clientId, clientSecret, and appOrigin.

Pre-request Script on the POST {{baseUrl}}{{tokenPath}} request:

const method = "POST";
const path = pm.environment.get("tokenPath");
const clientId = pm.environment.get("clientId");
const secret = pm.environment.get("clientSecret");

const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = CryptoJS.lib.WordArray.random(16).toString();
const bodyHash = CryptoJS.SHA256("").toString(CryptoJS.enc.Hex);

const canonical = [method, path, clientId, timestamp, nonce, bodyHash].join("\n");
const signature = CryptoJS.HmacSHA256(canonical, secret).toString(CryptoJS.enc.Base64);

pm.request.headers.upsert({ key: "X-Client-Id", value: clientId });
pm.request.headers.upsert({ key: "X-Timestamp", value: timestamp });
pm.request.headers.upsert({ key: "X-Nonce", value: nonce });
pm.request.headers.upsert({ key: "X-Signature", value: signature });

Tests script, saves the token:

pm.test("Status is 200", () => pm.response.to.have.status(200));
pm.environment.set("accessToken", pm.response.json().accessToken);

Protected function calls: Auth tab → Bearer Token → {{accessToken}}, plus header X-App-Origin: {{appOrigin}}. No signing needed there.

Troubleshooting invalid_signature: tokenPath must include the leading /, i.e. /api/auth/token, not api/auth/token. The server signs against HttpContext.Request.Path, which always starts with /; a missing leading slash silently produces a different canonical string and a different signature. console.log(JSON.stringify(canonical)) right after building it is the fastest way to spot this or any other stray character.

Behavior differences from the ASP.NET Core host

ASP.NET Core (ACYTEC.Security.Hmac.AspNetCore) Functions (this package)
Token endpoint route Configurable, ACYTECHmacSecurity:TokenEndpointPath Fixed at auth/token. Azure Functions HTTP trigger routes are compile time attribute values, not runtime configuration.
Per-endpoint scope requirement .RequireAuthorization(policy => policy.RequireClaim("scope", "...")) [RequireACYTECScope("...")]
Trigger parameter type N/A HttpRequestData + FunctionContext, see above, not HttpContext directly

Everything else, the HMAC signing scheme, token shape, origin validation, and AES-GCM payload envelope, is byte-for-byte identical, see PROTOCOL.md in the base package's repository.

Testing notes

The pipeline integration in ACYTECHmacFunctionsMiddleware depends on Microsoft.Azure.Functions.Worker.FunctionContext, an abstract SDK type that isn't practical to fully mock. The reflection-based scope resolution and token-path matching are covered by unit tests in this package's test project; the full pipeline (token issuance, bearer auth, origin validation, scope enforcement, and payload encryption, all together) was verified by actually running func start against samples/SampleFunctionsHostApi and driving it with real signed HTTP requests, not by a mocked test. If you change ACYTECHmacFunctionsMiddleware, re-verify against a real running Functions host, a mocked unit test would not have caught the HttpContext binding issue documented above.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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
1.0.0 33 9/1/2026