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/loginusing the emailadmin@dev.caand passwordPassword123to watch the Azure server dynamically append theAdminclaim 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: