WESLEY WITSKEN

Azure Functions Middleware

Sat Aug 16 2025

How to effectively use Middleware in Azure functions - with a less-trivial example.

Written by: Wesley Witsken

A soldier protecting a sleeping child.

Custom Authorization via Middleware in Azure Functions: Multi-Tenant Example

Introduction

If you’ve worked with Azure Functions Middleware in the isolated worker model, you’ve probably seen the same kinds of examples: exception handling, logging, performance tracking, maybe appending a trivial value into the FunctionContext.Items.

Those are useful—but they barely scratch the surface of what Middleware can do.

What I rarely see are examples where Middleware handles authorization. And yet, this is a powerful and practical scenario: securing webhook endpoints that expect specific headers, validating secrets, or enforcing request structure before the Function executes.

In this post, I’ll show you how Middleware can do exactly that, including a multi-tenant scenario where each request must provide a tenant_id and tenant_secret. We’ll also append a list of that tenant’s authorized applications into the FunctionContext.Items, so your Functions can decide whether to proceed.


Quick Recap: What Is Middleware in Azure Functions?

Middleware is code that runs before and after your Function executes. It’s your hook into the request pipeline.

Typical uses include:

  • Exception handling
  • Logging and telemetry
  • Performance measurement
  • Data enrichment (e.g., appending metadata to the context)

All valuable, but what if your concern isn’t how a Function runs—it’s whether it should run at all?


Why Authorization Belongs in Middleware

Many apps need customized authorization that doesn’t fit neatly into Azure’s built-in authentication options:

  • Webhook validation: Ensuring a request includes the right headers.
  • Tenant-level security: Requiring both a tenant_id and tenant_secret.
  • Application scoping: Attaching the tenant’s authorized apps to the request for downstream decisions.

Instead of sprinkling this validation logic into every Function, Middleware lets you:

  • Centralize security in one place.
  • Ensure all requests are checked consistently.
  • Keep Function code clean and focused on business logic.

Real-World Example: Multi-Tenant Authorization

Imagine you’re building an endpoint that receives webhooks from multiple tenants. Each tenant has a unique tenant_id and tenant_secret. For simplicity’s sake, in this example we’ll treat those as plaintext values sent in headers, even though in production you’d typically prefer hashing or token-based schemes.

Here’s what our Middleware should do:

  • Intercept every request.
  • Validate that both tenant_id and tenant_secret headers are present.
  • Use a cached Key Vault service to verify that the secret is correct for the given tenant.
  • Call another service to retrieve a list of authorized applications for that tenant.
  • Append the authorized apps into context.Items so consuming Functions can decide whether to allow or reject the request.

Implementing Multi-Tenant Authorization Middleware

public class TenantAuthMiddleware : IFunctionsWorkerMiddleware
{
    private readonly ILogger<TenantAuthMiddleware> _logger;
    private readonly ISecretCache _secretCache; // wraps Key Vault + cache
    private readonly IAuthorizedApplicationsService _authorizedApps;

    public TenantAuthMiddleware(
        ILogger<TenantAuthMiddleware> logger,
        ISecretCache secretCache,
        IAuthorizedApplicationsService authorizedApps)
    {
        _logger = logger;
        _secretCache = secretCache;
        _authorizedApps = authorizedApps;
    }

    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var req = await context.GetHttpRequestDataAsync();

        if (!await IsValidTenantAsync(req, context))
        {
            _logger.LogWarning("Rejected request: invalid tenant_id or tenant_secret.");

            var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
            await res.WriteStringAsync("Unauthorized");

            context.GetInvocationResult().Value = res;
            return;
        }

        await next(context); // continue pipeline
    }

    private async Task<bool> IsValidTenantAsync(HttpRequestData? req, FunctionContext context)
    {
        if (req == null) return false;

        if (!req.Headers.TryGetValues("tenant_id", out var idValues) ||
            !req.Headers.TryGetValues("tenant_secret", out var secretValues))
        {
            return false;
        }

        var tenantId = idValues.First();
        var tenantSecret = secretValues.First();

        // Validate against Key Vault (cached)
        var expectedSecret = await _secretCache.GetSecretAsync(tenantId);
        if (expectedSecret == null || tenantSecret != expectedSecret)
        {
            return false;
        }

        // Retrieve authorized apps
        var apps = await _authorizedApps.GetAuthorizedAppsAsync(tenantId);

        // Store in context for downstream use
        context.Items["AuthorizedApplications"] = apps;
        context.Items["TenantId"] = tenantId

        return true;
    }
}

Registering in Program.cs

Make sure to use the IHost builder, not the newer IHostApplication builder. Middleware is only supported in the older IHost builder version, like so. If you try to use middleware with IHostApplication builder, the FunctionContext will be null and no exceptions will be thrown.

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(worker =>
    {
        worker.UseWhen<TenantAuthMiddleware>(ctx =>
            ctx.FunctionDefinition.InputBindings.Values
                .Any(b => b.Type.Equals("httpTrigger", StringComparison.OrdinalIgnoreCase)));
    })
    .Build();

host.Run();

Best Practices & Lessons Learned

  • IHost builder: Always use the older IHost builder in Program.cs. Don’t use IHostApplication builder.
  • Middleware order matters: Authorization should run early in the pipeline.
  • Cache Key Vault lookups: Don’t hit Key Vault on every request—use memory caching.
  • Short-circuit unauthorized requests: Return immediately instead of letting the Function run.
  • Pass data forward cleanly: Using context.Items to pass tenant and app info keeps Functions focused on business logic.
  • Plaintext for simplicity: This example uses plaintext secrets, but you should adopt stronger security (e.g., HMACs, JWTs) in production if available.

What You Gain

By moving multi-tenant authorization into Middleware, you get:

  • Security: Unauthorized requests never hit your Function logic.
  • Maintainability: One place to change or expand validation rules.
  • Cleaner Functions: Handlers focus only on business logic.
  • Multi-Tenant Support: Authorized apps are available per-request, ready for downstream checks.

Conclusion

Middleware in Azure Functions goes far beyond exception handling or logging. Used effectively, it becomes a central place to enforce custom authorization and manage multi-tenant security.

By validating tenant identities, secrets, and authorized applications in the pipeline, you create a consistent and secure gate that every request must pass through. This not only simplifies your Function code but also strengthens your overall architecture.