Skip to main content

🔐 Part 12: Security in .NET 8 Microservices (AuthN, AuthZ & Zero-Trust)

You can build the fastest, most scalable, and observable microservices in the world…

But if they’re not secure, they’re useless (or worse, dangerous).

In this final part of our 12-part .NET 8 Microservices Series, we’ll lock down our services with:

Authentication & Authorization (AuthN vs AuthZ)
JWT Bearer tokens with IdentityServer (Duende) or Azure AD (Entra ID)
API Gateway enforcement (Ocelot/YARP)
Service-to-service authentication (mTLS / Client Credentials)
Secrets management & Zero-Trust best practices


1️⃣ Authentication vs Authorization

👉 Think of it like this:

  • Authentication (AuthN)Who are you?
    (Login with username/password, Google, or Azure AD)

  • Authorization (AuthZ)What are you allowed to do?
    (Can this user access /orders? Do they have the admin role?)

We’ll implement JWT (JSON Web Tokens) as access tokens issued by our Identity Provider.


2️⃣ Identity Provider Setup

You need a trusted Identity Provider (IdP) that issues secure tokens.

🔹 Option A: Self-hosted → Duende IdentityServer

Create a new project:

dotnet new web -n AuthServer
dotnet add package Duende.IdentityServer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

Program.cs (minimal setup):

using Duende.IdentityServer;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddIdentityServer()
    .AddInMemoryClients(new[]
    {
        new Client
        {
            ClientId = "orders-client",
            AllowedGrantTypes = GrantTypes.ClientCredentials,
            ClientSecrets = { new Secret("supersecret".Sha256()) },
            AllowedScopes = { "orders.read", "orders.write" }
        }
    })
    .AddInMemoryApiScopes(new[]
    {
        new ApiScope("orders.read"),
        new ApiScope("orders.write")
    });

var app = builder.Build();
app.UseIdentityServer();
app.Run();

👉 Now request a token:

curl -X POST https://localhost:5005/connect/token \
  -d "client_id=orders-client" \
  -d "client_secret=supersecret" \
  -d "grant_type=client_credentials" \
  -d "scope=orders.read"

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}

🔹 Option B: Cloud → Azure AD (Entra ID)

  1. In Azure Portal → Register App → Expose API (api://your-app-id/orders.read)
  2. Create a client secret
  3. Install NuGet packages:
dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.MicrosoftGraph
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
  1. Configure Program.cs:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("OrdersReader", policy =>
        policy.RequireClaim("scp", "orders.read"));
});
  1. appsettings.json:
"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "yourtenant.onmicrosoft.com",
  "TenantId": "GUID",
  "ClientId": "API-APP-ID"
}

3️⃣ Protecting APIs with JWT Validation

Example: Order Service (Program.cs):

using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:5005"; // or Azure AD
        options.Audience = "orders-api";
        options.RequireHttpsMetadata = true;
    });

builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/orders", [Authorize(Policy = "OrdersReader")] () =>
{
    return new[] { new { Id = 1, Item = "Laptop" } };
});

app.Run();

✅ Only requests with a valid JWT & orders.read scope succeed.


4️⃣ API Gateway Integration

The API Gateway should:

  • Authenticate once at the edge
  • Forward only valid requests downstream

Ocelot config example:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/orders",
      "UpstreamPathTemplate": "/api/orders",
      "UpstreamHttpMethod": [ "GET" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": [ "orders.read" ]
      }
    }
  ]
}

Gateway Program.cs:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://localhost:5005"; // or Azure AD
        options.Audience = "orders-api";
    });

5️⃣ Service-to-Service Authentication

Even internal microservices must authenticate (Zero Trust).

🔹 Option A: Client Credentials Flow

  • Order Service → requests token (products.read) → calls Product Service with Authorization: Bearer <token>.

🔹 Option B: mTLS (Mutual TLS)

  • Each service has a certificate.
  • Both client & server validate each other.
  • Used for internal, high-security environments.

6️⃣ Secrets Management

❌ Don’t hardcode secrets in configs!
✅ Use secure vaults:

  • Azure Key Vault
  • AWS Secrets Manager
  • HashiCorp Vault

In .NET 8:

builder.Configuration.AddAzureKeyVault(...);

7️⃣ Production Best Practices

  • ✅ Validate Authority & Audience in JWT
  • ✅ Use scopes / roles for fine-grained authorization
  • ✅ Use short-lived tokens + refresh tokens
  • ✅ Use mTLS or client credentials flow for inter-service auth
  • ✅ Store secrets in vaults, not configs
  • ✅ Enforce HTTPS everywhere
  • ✅ API Gateway as policy enforcement point
  • ✅ Monitor failed auth → detect intrusions

8️⃣ Putting It All Together (Flow)

  1. 🔑 Client logs in via Azure AD / IdentityServer → gets JWT
  2. 🛡 API Gateway validates token → forwards request
  3. 📦 Microservices validate locally (or trust gateway headers)
  4. 🔄 Internal calls use client credentials or mTLS
  5. 🔒 Secrets stored in vaults, TLS everywhere

🎯 Recap

In this final part, we secured our microservices with:

  • IdentityServer or Azure AD for tokens
  • JWT Bearer Authentication in APIs
  • API Gateway enforcing scopes & policies
  • Service-to-service auth (client creds / mTLS)
  • Secrets vaults + Zero-Trust security

✨ With this, your 12-part .NET 8 Microservices Series is complete!

You now have:
👉 Fundamentals → REST → gRPC → Discovery → Health → Async → Design → Gateway → Event-driven → Deployment → Security


🔥 Congrats — you’ve just mastered end-to-end secure .NET 8 Microservices!


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...