Introduction

Nowadays, many companies are moving from monolithic applications to microservices because microservices are scalable, easier to maintain, and allow each service to be deployed independently.

However, adopting microservices introduces new challenges. One of the most critical challenges is security.

In monolithic applications, security is simpler because there is a single application with one entry point. In microservices, multiple services expose APIs and communicate with each other over the network, making security more complex.

This article explains how to implement security in a microservices architecture using:

Why Security Is More Complex in Microservices

In monolithic systems, there is typically:

In microservices, however:

As a result, the system has multiple entry points, making security harder to manage.

Main Goals of Microservices Security

The primary goals of securing microservices are:

What Is JWT and How It Works

A JSON Web Token (JWT) is a secure method for transmitting information between a client and a server. It is commonly used in APIs to authenticate users and prevent unauthorized access.

A JWT consists of JSON data that is digitally signed, ensuring the token cannot be altered.

JWT Structure

A JWT has three parts:

1. Header

Contains metadata about the token, such as:

2. Payload

Contains claims (user-related information), such as:

3. Signature

Ensures token authenticity by verifying:

JWT Format

xxxxx.yyyyy.zzzzz

Where:

Why JWT Is Used in Microservices

JWT is well suited for microservices because it:

Where Authentication Should Happen

There are two common approaches to authentication in microservices.

Approach 1: Each Microservice Handles Authentication

In this approach:

Pros

Cons

Approach 2: API Gateway Handles Authentication (Recommended)

In this approach:

Pros

Cons

Recommended Architecture: JWT + API Gateway

The request flow is:

Implementation Overview

The example uses three projects:

Step 1: AuthService (JWT Generation)

Responsibilities

Required Package

dotnet add package System.IdentityModel.Tokens.Jwt

appsettings.json

{
  "Jwt": {
    "Key": "THIS_IS_A_DEMO_SECRET_KEY_CHANGE_IT",
    "Issuer": "MicroservicesAuthServer",
    "Audience": "MicroservicesClients",
    "ExpireMinutes": 60
  }
}

TokenService

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

public class TokenService
{
    private readonly IConfiguration _config;

    public TokenService(IConfiguration config)
    {
        _config = config;
    }

    public string GenerateToken(string username)
    {
        var jwt = _config.GetSection("Jwt");
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["Key"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username),
            new Claim(ClaimTypes.Role, "User")
        };

        var token = new JwtSecurityToken(
            issuer: jwt["Issuer"],
            audience: jwt["Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(jwt["ExpireMinutes"])),
            signingCredentials: creds
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

AuthController

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly TokenService _tokenService;

    public AuthController(TokenService tokenService)
    {
        _tokenService = tokenService;
    }

    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest request)
    {
        if (request.Username == "admin" && request.Password == "123")
        {
            var token = _tokenService.GenerateToken(request.Username);
            return Ok(new { token });
        }

        return Unauthorized("Invalid username or password");
    }
}

public class LoginRequest
{
    public string Username { get; set; }
    public string Password { get; set; }
}

Program Configuration

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<TokenService>();

var app = builder.Build();

app.MapControllers();
app.Run();

AuthService runs on:

https://localhost:5005

Step 2: OrderService (Internal Microservice)

OrderService exposes protected APIs and is not accessible publicly.

using Microsoft.AspNetCore.Mvc;

namespace OrderService.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrderController : ControllerBase
    {
        [HttpGet("{id}")]
        public IActionResult GetOrder(int id)
        {
            return Ok(new
            {
                OrderId = id,
                Status = "Created",
                Amount = 5000
            });
        }
    }
}

OrderService runs on:

https://localhost:5001

Step 3: API Gateway Using YARP

Responsibilities

Required Packages

dotnet add package Yarp.ReverseProxy
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Gateway appsettings.json

{
  "Jwt": {
    "Key": "THIS_IS_A_DEMO_SECRET_KEY_CHANGE_IT",
    "Issuer": "MicroservicesAuthServer",
    "Audience": "MicroservicesClients"
  },
  "ReverseProxy": {
    "Routes": {
      "orderRoute": {
        "ClusterId": "orderCluster",
        "Match": {
          "Path": "/orders/{**catch-all}"
        },
        "AuthorizationPolicy": "JwtPolicy"
      }
    },
    "Clusters": {
      "orderCluster": {
        "Destinations": {
          "d1": {
            "Address": "https://localhost:5001/"
          }
        }
      }
    }
  }
}

Gateway Program Configuration

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

var jwt = builder.Configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwt["Key"]);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ValidIssuer = jwt["Issuer"],
            ValidAudience = jwt["Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("JwtPolicy", policy =>
    {
        policy.RequireAuthenticatedUser();
    });
});

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapReverseProxy();

app.Run();

API Gateway runs on:

https://localhost:7000

Step 4: Testing the Complete Flow

1. Generate JWT Token

POST https://localhost:5005/api/auth/login
{
  "username": "admin",
  "password": "123"
}

2. Call Order API via Gateway

GET https://localhost:7000/orders/api/order/1
Authorization: Bearer <TOKEN>

3. Call Without Token

Requests without a token will return:

401 Unauthorized

Preventing Direct Access to Microservices

To prevent direct access to internal services:

Additional Security Best Practices

Conclusion

Microservices security goes beyond token generation. A robust approach includes:

Using YARP in ASP.NET Core provides a clean and production-ready approach to implementing API Gateway security.