Hardening .NET Minimal APIs: Shifting from Local Dev to Azure-backed RBAC


Building a local proof-of-concept is easy, but bringing enterprise-grade security to the cloud requires intentional architecture. In this write-up, we walk through scaling a .NET 8 Minimal API authentication engine—codenamed GatekeeperAuth—from a local Mac sandbox to an automated, multi-role deployment running securely on Azure App Service.

This is Part 1 of a two-part series. Part 2 — Bridging the Gap: Connecting React (Azure SWA) to a Secured .NET 8 API — covers wiring a React frontend hosted on Azure Static Web Apps to this secured backend.

The Architectural Blueprint

Moving code out of a local sandbox means structuring authorization logic to handle granular permissions. Rather than using an all-or-nothing authentication gate, we implemented Role-Based Access Control (RBAC) via Claims-Based Authorization directly inside the middleware pipeline.

graph TD
    A[Client Request] --> B[Authentication Middleware]
    B -->|Cookie Valid?| C[Authorization Layer]
    B -->|No Cookie| D[401 Unauthorized]
    C -->|Has Role: Practitioner| E[/api/secure/dashboard]
    C -->|Has Role: Admin| F[/api/secure/delete-record]
    C -->|Role Insufficient| G[403 Forbidden]

1. The .NET 8 RBAC Implementation

By utilizing standard cookie authentication events, we intercepted unauthorized access explicitly to return native API status codes (401 Unauthorized and 403 Forbidden) instead of automated MVC webpage redirects.

Here is how the core pipeline is configured in Program.cs:

C#
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "GatekeeperSession";
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // Forced SSL in Cloud
        options.ExpireTimeSpan = TimeSpan.FromMinutes(20);

        // Return 401 instead of redirecting to a login page
        options.Events.OnRedirectToLogin = context => {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            return Task.CompletedTask;
        };

        // Return 403 when identities are known but permissions are insufficient
        options.Events.OnRedirectToAccessDenied = context => {
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
            return Task.CompletedTask;
        };
    });

Guarding the Endpoints

With the authorization middleware injected, declaring standard endpoints vs. administrative-only destinations becomes perfectly clean using the .RequireAuthorization() policy modifiers:

C#
// Standard Protected Endpoint: Accessible by any logged-in user
app.MapGet("/api/secure/dashboard", () => Results.Ok("Welcome back!"))
   .RequireAuthorization();

// Destructive RBAC Endpoint: Strictly locked down to Administrators
app.MapDelete("/api/secure/delete-record/{id}", (int id) =>
   Results.Ok($"Record {id} permanently purged."))
   .RequireAuthorization(policy => policy.RequireRole("Admin"));

2. Automating the Pipeline to Azure

Hardcoding operations via localized CLI commands scales poorly. To achieve a zero-friction workflow, we established a CI/CD Pipeline linking GitHub repositories directly with Azure App Service utilizing an isolated Linux-based container runtime.

Resolving Local and Remote Divergences

During automated configuration handshakes, Git repositories can diverge when remote configuration assets (like automated workflow files generated by Azure) match local commits asynchronously.

To merge distinct structural timelines without forcing unnecessary workspace rewrites, we can safely reset a locked MERGE_HEAD state and force a non-rebased downstream pull:

# Abort any background-stuck merge blocks
git merge --abort

# Capture remote configuration changes without linear rewrite conflicts
git pull origin main --no-rebase

# Safely push the combined local feature branches
git push origin main

3. The Result

With this architecture implemented:

Every standard git push triggers an isolated GitHub Runner to rebuild, test, and host the binary to Azure seamlessly.

The authentication context seamlessly isolates roles, shifting from validation gates to explicit claim authorization blocks automatically.

The next evolutionary iteration for this module involves wrapping custom IMiddleware pre-request hooks to achieve domain-routed, multi-tenant isolation.

Try It Live

The production API is actively running on Azure. You can execute requests directly against the live environment to test the RBAC rules using a tool like Postman, Thunder Client, or curl:

  • Base Live URL: https://gatekeeperauth-gsevezf3d8awc8fj.centralus-01.azurewebsites.net
  • Test the restriction (GET): https://gatekeeperauth-gsevezf3d8awc8fj.centralus-01.azurewebsites.net/api/secure/dashboard

💡 Quick Test: Fire a POST request to /api/auth/login using the email admin@dev.ca and password Password123 to watch the Azure server dynamically append the Admin claim and drop your encrypted session cookie live!


What’s Next

The backend is production-ready — but an API without a UI is only half the story. In Part 2 we introduce a React frontend hosted on Azure Static Web Apps and wire it directly to this secured API, solving the cross-domain cookie challenge along the way.

Continue reading: Bridging the Gap: Connecting React (Azure SWA) to a Secured .NET 8 API




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Migrating marketingPy from Docker Compose to AWS Lambda
  • Bridging the Gap: Connecting React (Azure SWA) to a Secured .NET 8 API
  • Google Calendar API Bidirectionally with Full Calendar in a Multi-Tenant Booking Engines
  • How to restrict Multi-Tenant Bookings + Google Accounts Separation
  • Setting Up Google Calendar API & OAuth 2.0 for Multi-Tenant Booking Engines