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:
Many services run independently
Each service exposes its own API endpoints
Services communicate over the network
Services scale dynamically
IP addresses and ports change frequently
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:
Correctly authenticate users
Authorize users based on roles or permissions
Ensure only trusted systems can call internal services
Prevent direct access to microservices
Secure service-to-service communication
Protect tokens and sensitive data
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:
xxxxx – Header
yyyyy – Payload
zzzzz – Signature
Why JWT Is Used in Microservices
JWT is well suited for microservices because it:
Is stateless (no server-side sessions)
Includes authentication data in each request
Works well in distributed environments
Supports claims and roles
Is easy to validate
Where Authentication Should Happen
There are two common approaches to authentication in microservices.
Approach 1: Each Microservice Handles Authentication
In this approach:
Pros
Cons
Duplicate security logic across services
Harder to manage policies
Increased configuration and development effort
Approach 2: API Gateway Handles Authentication (Recommended)
In this approach:
The client sends the JWT token to the API Gateway
The gateway validates the token
Requests are forwarded to internal services
Microservices remain private
Pros
Cons
Recommended Architecture: JWT + API Gateway
The request flow is:
Client authenticates with AuthService and receives a JWT
Client sends requests to the API Gateway with the JWT
Gateway validates the token
Gateway routes the request to the target microservice
Response flows back through the gateway
Implementation Overview
The example uses three projects:
AuthService – Generates JWT tokens
ApiGateway – Uses YARP for routing and JWT validation
OrderService – Internal service protected behind the gateway
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:
Deploy services inside a private network
Expose only the API Gateway publicly
Use firewall rules or security groups
In Kubernetes, use ClusterIP services
Additional Security Best Practices
Use HTTPS everywhere
Implement refresh tokens
Apply role-based authorization
Add rate limiting at the gateway
Enable centralized logging and monitoring
Conclusion
Microservices security goes beyond token generation. A robust approach includes:
Issuing tokens from an authentication service
Validating tokens at the API Gateway
Routing requests only after validation
Keeping microservices private
Securing internal communication
Using YARP in ASP.NET Core provides a clean and production-ready approach to implementing API Gateway security.