From e6f41f958f552137dc8ebf1aa78fa81dcbad62e4 Mon Sep 17 00:00:00 2001 From: Isaiah Clifford Opoku Date: Thu, 21 May 2026 23:51:11 +0000 Subject: [PATCH] Add middleware for auth stubbing, logging, and context Introduced `AuthStubMiddleware` to bypass Azure AD authentication and `ImdsMiddleware` to simulate Azure Managed Identity locally. Enhanced `RequestContextMiddleware` to populate `HttpContext.Items` with request-specific context. Improved `RequestLoggingMiddleware` to log request/response details and processing time. These changes simplify local development and improve observability. --- src/AzLocal.Middleware/AuthStubMiddleware.cs | 24 +++++++++++++++++-- src/AzLocal.Middleware/ImdsMiddleware.cs | 22 ++++++++++++++++- .../RequestContextMiddleware.cs | 10 +++++--- .../RequestLoggingMiddleware.cs | 19 +++++++++++++-- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/AzLocal.Middleware/AuthStubMiddleware.cs b/src/AzLocal.Middleware/AuthStubMiddleware.cs index 5101431..61f9d55 100644 --- a/src/AzLocal.Middleware/AuthStubMiddleware.cs +++ b/src/AzLocal.Middleware/AuthStubMiddleware.cs @@ -1,12 +1,32 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; +using System.Security.Claims; namespace AzLocal.Middleware; +/// +/// Bypasses Azure AD authentication for local development. +/// Injects a fake authenticated identity on every request so [Authorize] checks pass +/// without needing a real Azure AD token or tenant. +/// public class AuthStubMiddleware { private readonly RequestDelegate _next; + // Static so the same fake identity is reused on every request — no per-request allocation. + // authenticationType must be non-null/non-empty for IsAuthenticated to return true. + private static readonly ClaimsPrincipal FakeUser = new(new ClaimsIdentity( + [ + new Claim("oid", "00000000-0000-0000-0000-000000000001"), // fake Azure object ID + new Claim("tid", "00000000-0000-0000-0000-000000000002"), // fake tenant ID + new Claim(ClaimTypes.Name, "azlocal-dev"), + ], authenticationType: "AzLocalStub")); + public AuthStubMiddleware(RequestDelegate next) => _next = next; - public async Task InvokeAsync(HttpContext context) => await _next(context); + public async Task InvokeAsync(HttpContext context) + { + // Skip real token validation — stamp every request as authenticated locally. + context.User = FakeUser; + await _next(context); + } } diff --git a/src/AzLocal.Middleware/ImdsMiddleware.cs b/src/AzLocal.Middleware/ImdsMiddleware.cs index de1670a..60a5d0e 100644 --- a/src/AzLocal.Middleware/ImdsMiddleware.cs +++ b/src/AzLocal.Middleware/ImdsMiddleware.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using System.Text.Json; namespace AzLocal.Middleware; @@ -6,8 +7,27 @@ public class ImdsMiddleware { private readonly RequestDelegate _next; + private static readonly string FakeTokenJson = JsonSerializer.Serialize(new + { + access_token = "fake-azlocal-token", + expires_on = "99999999999", + resource = "https://management.azure.com/", + token_type = "Bearer" + }); + public ImdsMiddleware(RequestDelegate next) => _next = next; - public async Task InvokeAsync(HttpContext context) => await _next(context); + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/metadata/identity/oauth2/token")) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = 200; + await context.Response.WriteAsync(FakeTokenJson); + return; // Do NOT call _next — response is already written + } + await _next(context); + } } + diff --git a/src/AzLocal.Middleware/RequestContextMiddleware.cs b/src/AzLocal.Middleware/RequestContextMiddleware.cs index 6ae17cc..76a18f8 100644 --- a/src/AzLocal.Middleware/RequestContextMiddleware.cs +++ b/src/AzLocal.Middleware/RequestContextMiddleware.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Http; +using AzLocal.Core.Routing; +using Microsoft.AspNetCore.Http; namespace AzLocal.Middleware; public class RequestContextMiddleware { private readonly RequestDelegate _next; - public RequestContextMiddleware(RequestDelegate next) => _next = next; - public async Task InvokeAsync(HttpContext context) => await _next(context); + public async Task InvokeAsync(HttpContext context) + { + context.Items["RequestContext"] = ServiceRouter.BuildContext(context.Request); + await _next(context); + } } diff --git a/src/AzLocal.Middleware/RequestLoggingMiddleware.cs b/src/AzLocal.Middleware/RequestLoggingMiddleware.cs index 37c5ff0..3fb234c 100644 --- a/src/AzLocal.Middleware/RequestLoggingMiddleware.cs +++ b/src/AzLocal.Middleware/RequestLoggingMiddleware.cs @@ -1,13 +1,28 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace AzLocal.Middleware; public class RequestLoggingMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; - public RequestLoggingMiddleware(RequestDelegate next) => _next = next; + public RequestLoggingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } - public async Task InvokeAsync(HttpContext context) => await _next(context); + public async Task InvokeAsync(HttpContext context) + { + var sw = Stopwatch.StartNew(); + _logger.LogDebug("→ {Method} {Path}", context.Request.Method, context.Request.Path); + await _next(context); + _logger.LogInformation("← {StatusCode} {Method} {Path} in {Elapsed}ms", + context.Response.StatusCode, context.Request.Method, + context.Request.Path, sw.ElapsedMilliseconds); + } }