Back to: Microservices using ASP.NET Core Web API Tutorials
JWT Authentication in Ocelot API Gateway
We have multiple ASP.NET Core microservices (User, Product, Order, Payment, Notification). Each service already enforces its own authorization rules with attributes and policies:
- Some endpoints are Public (no token required).
- Some endpoints require a Valid JWT.
- Some endpoints require specific roles or claims (RBAC).
The API Gateway (Ocelot) sits in front of them as the Single-Entry Point. The gateway Does Not Know which specific endpoints are public vs. protected for every service, and we don’t want to duplicate that knowledge in Ocelot (too fragile and hard to maintain).
So, we will make the gateway Auth-Aware (it understands and can validate access tokens) but not Auth-Authoritative (it doesn’t declare endpoint-level rules). In other words:
- If a request carries a Bearer Token, the gateway Validates it and rejects bad tokens early (fail-fast).
- If a request Does Not carry a token, the gateway passes it through so public endpoints keep working.
- The microservices remain the source of truth for per-endpoint authorization: [AllowAnonymous], [Authorize], [Authorize(Roles=”…”)], and custom policies continue to do the actual access control.
This gives us correctness (rules live with the code that owns them), performance (bad tokens get blocked at the edge), and simplicity (no duplicated route rules in Ocelot).
The Real-World Scenario We’re Addressing
Imagine your system as a shopping mall:
- The API Gateway is the Security Gate at the entrance.
- The Microservices are different shops inside, UserService, ProductService, OrderService, etc.
- Each shop has its Own Internal Policies (who can enter, who can buy, who can manage).
Here’s how that plays out technically:
- A New Customer registers → The request has No Token, so the Gateway passes it to UserService.
- A Logged-In Customer wants to view their orders → The request includes a Bearer Token; the Gateway verifies it, then forwards it to OrderService.
- A Malicious User tries to fake or reuse an old token → The Gateway detects that it’s invalid or expired and stops it, returning a 401 Unauthorized.
- An Admin user tries to perform a privileged action → The Gateway validates the token; the OrderService then checks the role claim inside the token to confirm admin rights.
Every request, whether public or protected, flows through the Gateway first. That’s what makes the Gateway the single point of enforcement for authentication.
Understanding Authentication vs Authorization
Before we go deeper, let’s clearly understand Authentication and Authorization:
- Authentication is about who you are – validating identity through a JWT token.
- Authorization is about what you can do – deciding permissions (like Admin vs Customer).
In our architecture:
- Authentication happens at the Gateway.
- Authorization happens inside the microservices.
This separation is deliberate and powerful because:
- It keeps the Gateway light and general-purpose.
- It allows each microservice to maintain its own business rules independently.
- It prevents cross-service coupling; the Gateway doesn’t need to know your domain rules.
So, the Gateway only checks: “Is this token real, valid, and untampered?” It doesn’t care whether the user is a customer, vendor, or admin; that’s the microservice’s responsibility.
Why JWT Validation Belongs at the Gateway
A JWT (JSON Web Token) is a small, self-contained identity document. It’s signed using a secret key, and anyone possessing that key can verify whether the token is valid and unaltered. Validating JWTs at the Gateway layer gives you Multiple Benefits:
- Security: Invalid or expired tokens never reach your internal services.
- Efficiency: You save internal network bandwidth by rejecting unauthorized traffic early.
- Consistency: Every request across all services gets validated uniformly in one place.
- Simpler microservices: Each service trusts that any token reaching it is already verified for authenticity.
Essentially, the Gateway becomes your First Layer of Defense.
Implementing JWT Token Validation at the API Gateway:
Let us proceed to understand how to implement JWT Token Validation in the API Gateway project, step by step.
Step 1: Installing the JWT Authentication Package
In the API Gateway project, we need to validate JWT tokens for authentication. ASP.NET Core doesn’t support JWT out of the box; it’s modular. So, to enable this functionality, we install the Microsoft.AspNetCore.Authentication.JwtBearer package. This package lets the gateway read and validate incoming Bearer tokens in the Authorization header, ensuring that only valid tokens reach the system. Without it, the gateway would have no way to understand JWT tokens. So, in the APIGateway project, please install the following package.
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Key Points
- Enables JWT token validation middleware in ASP.NET Core.
- Allows parsing and verifying Authorization: Bearer {token} headers.
- Required for calling AddJwtBearer(…) service in Program.cs.
- Essential to authenticate tokens before forwarding requests.
- Prevents tampered or expired tokens from entering the system.
Add JWT settings to appsettings.json
We store our JWT configuration values, such as the issuer and secret key, in appsettings.json to keep things clean and configurable. These values help the Gateway correctly validate incoming tokens. The issuer tells the system who created the token, while the secret key is used to verify its digital signature. Keeping these in a config file rather than hardcoding them makes the app more flexible and environmentally friendly. So, please modify the appsetings.json as follows for our APIGateway project to include JWT Configuration keys:
{
"AllowedHosts": "*",
"JwtSettings": {
"Issuer": "UserService.API",
"SecretKey": "fPXxcJw8TW5sA+S4rl4tIPcKk+oXAqoRBo+1s2yjUS4="
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Error",
"System": "Error",
"Ocelot": "Error"
}
},
"Properties": {
"Application": "APIGateway",
"Environment": "Development"
},
"WriteTo": [
{
"Name": "Async",
"Args": {
"configure": [
{
"Name": "Console",
"Args": {
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{Application}/{Environment}] CorrelationId={CorrelationId} {Message:lj}{NewLine}{Exception}"
}
}
]
}
},
{
"Name": "Async",
"Args": {
"configure": [
{
"Name": "File",
"Args": {
"path": "logs/UserService-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 30,
"shared": true,
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{Application}/{Environment}] CorrelationId={CorrelationId} {Message:lj}{NewLine}{Exception}"
}
}
]
}
}
]
}
}
Key Points:
- Issuer: Name of the service that created the token (e.g., UserService).
- SecretKey: Used to verify that the token hasn’t been altered.
- Stored in config so they can change per environment (dev/prod).
- Used inside TokenValidationParameters during JWT setup.
- Avoids hardcoding sensitive values directly in code.
Creating a Custom Middleware:
This middleware is the gatekeeper for all requests. It checks if an incoming request includes a Bearer token. If a token exists, the middleware tries to authenticate it. If it’s invalid or expired, the middleware stops the request and returns a 401 Unauthorized. On the other hand, if it is valid, it attaches the user info (ClaimsPrincipal) to the request context so downstream logging or tracing can use it. If there’s no token at all, the middleware lets the request pass through, which is perfect for public APIs.
Create a class file named GatewayBearerValidationMiddleware.cs within the Middlewares folder, then copy-paste the following code.
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace APIGateway.Middlewares
{
// Edge validator for Bearer tokens:
// - If an Authorization: Bearer token is present, authenticate it at the gateway.
// - If invalid/expired, short-circuit with 401 (fail-fast).
// - If no token, pass through (so public endpoints still work).
// Per-endpoint authorization remains in downstream services.
public sealed class GatewayBearerValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly string _scheme;
public GatewayBearerValidationMiddleware(
RequestDelegate next,
string scheme = JwtBearerDefaults.AuthenticationScheme)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_scheme = scheme;
}
public async Task InvokeAsync(HttpContext context, IAuthenticationService auth)
{
var authHeader = context.Request.Headers["Authorization"].ToString();
if (!string.IsNullOrWhiteSpace(authHeader) &&
authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var result = await auth.AuthenticateAsync(context, _scheme);
if (!result.Succeeded)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers["WWW-Authenticate"] =
"Bearer error=\"invalid_token\", error_description=\"Invalid or expired access token.\"";
context.Response.ContentType = "application/json; charset=utf-8";
var problem = new
{
type = "https://httpstatuses.com/401",
title = "Unauthorized",
status = 401,
detail = "Invalid or expired access token.",
traceId = context.TraceIdentifier,
timestamp = DateTimeOffset.UtcNow
};
if (!context.Response.HasStarted)
{
await context.Response.WriteAsync(JsonSerializer.Serialize(problem));
}
// IMPORTANT: stop the pipeline on invalid token
return;
}
// Attach principal for logging/correlation at the edge
context.User = result.Principal!;
}
await _next(context);
}
}
// Registration helper for GatewayBearerValidationMiddleware.
public static class GatewayBearerValidationExtensions
{
public static IApplicationBuilder UseGatewayBearerValidation(
this IApplicationBuilder app,
string scheme = JwtBearerDefaults.AuthenticationScheme)
{
return app.UseMiddleware<GatewayBearerValidationMiddleware>(scheme);
}
}
}
Key Points:
- Only checks the token if it exists (public endpoints still work).
- Authenticates the token using ASP.NET Core’s authentication pipeline.
- Returns 401 Unauthorized immediately if the token is invalid.
- Attaches the user’s identity to the HttpContext.User if valid.
- Doesn’t interfere with downstream services’ [Authorize] attributes.
Configure JWT, Middlewares, and Ocelot in Program.cs (Gateway)
In Program.cs, we wire up the entire authentication flow. First, we configure JWT token validation using the values from appsettings.json. Then, we register our custom middleware to inspect incoming requests. If a token is present, it will be validated at the gateway itself. If invalid, the request is rejected. After the middlewares are run, Ocelot takes over and routes the request to the correct microservice. This setup keeps your architecture clean: the gateway validates token integrity, and the microservices decide who can access what. So, please modify the Program class as follows to add JWT Authentication.
using APIGateway.Middlewares;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Serilog;
using System.Text;
namespace APIGateway
{
public class Program
{
public async static Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// ---------------------------------------------------------------------
// Configure Serilog as the application's main logging provider.
// ---------------------------------------------------------------------
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.CreateLogger();
// ---------------------------------------------------------------------
// Replace the default .NET logging system with Serilog.
// ---------------------------------------------------------------------
builder.Host.UseSerilog();
// JWT Authentication (edge validation when token is present)
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]!)
),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
// ---------------------------------------------------------------
// Load Ocelot Configuration
// ---------------------------------------------------------------
builder.Configuration.AddJsonFile(
"ocelot.json",
optional: false,
reloadOnChange: true
);
// ---------------------------------------------------------------
// Register Ocelot Services
// ---------------------------------------------------------------
builder.Services.AddOcelot(builder.Configuration);
var app = builder.Build();
app.UseHttpsRedirection();
// ---------------------------------------------------------------------
// Add custom middleware before Ocelot.
// ---------------------------------------------------------------------
app.UseGatewayBearerValidation();
app.UseCorrelationId();
app.UseRequestResponseLogging();
// ---------------------------------------------------------------
// Register Ocelot Middleware (Core Gateway Logic)
// ---------------------------------------------------------------
// IMPORTANT:
// This MUST be the LAST middleware in the pipeline,
// Once Ocelot handles a request, no other middleware executes afterward.
await app.UseOcelot();
app.Run();
}
}
}
Code Explanations:
AddAuthentication():
- This sets up the Authentication System in ASP.NET Core.
- It tells the application: I will be using authentication, and this is the default scheme to use.
- Without it, the app wouldn’t know how to handle any kind of authentication request.
AddJwtBearer()
- This registers the JWT Validation Engine.
- It defines how the app should validate tokens, for example, which Issuer to trust, which SecretKey to use, and whether to check expiration or signature.
- It’s like teaching the system how to recognize a valid token.
GatewayBearerValidationMiddleware()
This is your custom traffic guard. It runs before Ocelot routes the request. It checks:
- If there’s no token → let the request pass through (for public endpoints).
- If there’s a token → calls the authentication system (configured by AddJwtBearer()) to validate it.
- If the token is invalid → stops the request and returns 401 Unauthorized.
- If valid → attaches the user info (HttpContext.User) and forwards the request to Ocelot.
How They Work Together
- AddAuthentication() and AddJwtBearer() set up the rules and engine for JWT validation.
- GatewayBearerValidationMiddleware invokes that engine whenever a request arrives with a token.
- If the token passes validation, the user claims are attached to the context.
- Finally, Ocelot forwards the request (with or without user identity) to the correct microservice.
Do Microservices Need to Validate the Token Again?
Yes, each microservice still needs to validate the token, but not in the same way as the Gateway. Here’s why: The API Gateway validates the token at the edge to block invalid or expired tokens from entering your internal network. But once a request passes through the Gateway, every microservice still needs to trust but verify that the token is genuine and unchanged, because microservices are independent and must not assume that other services always protect them.
So, each microservice performs its own lightweight JWT validation to:
- Confirm the token is still valid (not tampered with).
- Extract user identity and roles from the token.
- Apply its own authorization logic (like [Authorize] and [Authorize(Roles=”Admin”)]).
This keeps every service secure and autonomous, even if a single layer (such as the Gateway) fails or is misconfigured.
Simple Analogy
Think of the Gateway as an Airport Entrance Check, and the microservices as Individual Gates.
- The Gateway checks your boarding pass and ID to make sure you’re allowed inside the airport.
- Each boarding gate still scans your pass before letting you on the plane — because it’s responsible for its own passengers.
Similarly, each microservice must validate the token before serving protected data.
Does Double Token Validation Impact Performance?
No, validating the token at both the API Gateway and the microservices does not significantly impact performance.
- JWT validation is lightweight, in-memory, and stateless.
- It does not hit the database, so the overhead is negligible.
- Even at a large scale, the overhead is only a few microseconds per request, not milliseconds.
What’s the Best Industry Approach?
- Validate token at the Gateway → to block invalid/tampered tokens early.
- Validate again in Microservices to enforce access control (e.g., roles, policies).
- This is the standard approach in secure microservice architectures (used by Netflix, Google, AWS, etc.), balancing strong security with negligible performance cost.
Bottom line: Double validation is secure, fast, and industry-approved.
Testing Product Service with JWT Token:
Installing JWT Token Package in ProductService.API Project.
Please install the following package.
- Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Add JWT Settings in AppSettings.json file:
Please modify the appsetings.json as follows of our ProductService.API project:
{
"ConnectionStrings": {
"DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ProductServiceDB;Trusted_Connection=True;TrustServerCertificate=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"RabbitMq": {
"HostName": "localhost",
"Port": 5672,
"UserName": "ecommerce_user",
"Password": "Test@1234",
"VirtualHost": "ecommerce_vhost"
},
"JwtSettings": {
"Issuer": "UserService.API",
"SecretKey": "fPXxcJw8TW5sA+S4rl4tIPcKk+oXAqoRBo+1s2yjUS4="
}
}
Configure JWT Token Validation Program.cs Class file
Please modify the Program class file in our ProductService as follows.API project:
using Messaging.Common.Extensions;
using Messaging.Common.Options;
using Messaging.Common.Publishing;
using Messaging.Common.Topology;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using ProductService.Application.Interfaces;
using ProductService.Application.Mappings;
using ProductService.Application.Messaging;
using ProductService.Application.Services;
using ProductService.Contracts.Messaging;
using ProductService.Domain.Repositories;
using ProductService.Infrastructure.Messaging.Extensions;
using ProductService.Infrastructure.Messaging.Producers;
using ProductService.Infrastructure.Persistence;
using ProductService.Infrastructure.Repositories;
using RabbitMQ.Client;
using System.Text;
using System.Text.Json.Serialization;
namespace ProductService.API
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add DbContext
builder.Services.AddDbContext<ProductDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Register repositories
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductImageRepository, ProductImageRepository>();
builder.Services.AddScoped<IDiscountRepository, DiscountRepository>();
builder.Services.AddScoped<IInventoryRepository, InventoryRepository>();
builder.Services.AddScoped<IReviewRepository, ReviewRepository>();
// Register services
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IProductService, ProductService.Application.Services.ProductService>();
builder.Services.AddScoped<IProductImageService, ProductImageService>();
builder.Services.AddScoped<IDiscountService, DiscountService>();
builder.Services.AddScoped<IReviewService, ReviewService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
// Add AutoMapper
builder.Services.AddAutoMapper(typeof(MappingProfile));
// RABBITMQ CONFIGURATION & SAGA PATTERN INTEGRATION SECTION
// 1️. Load RabbitMQ configuration (from appsettings.json → "RabbitMq" section)
// These options define host, credentials, exchange, routing keys, and queue names
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq"));
// Extract the strongly-typed RabbitMqOptions object so we can reuse its values directly
var mq = builder.Configuration.GetSection("RabbitMq").Get<RabbitMqOptions>()!;
// 2️. Register RabbitMQ Connection and Channel
// - Uses Messaging.Common.Extensions.AddRabbitMq() to:
// • Create a single long-lived connection to RabbitMQ.
// • Create and register a shared IModel (channel) used by both producers and consumers.
// - This ensures each microservice uses consistent topology and avoids connection leaks.
builder.Services.AddRabbitMq(mq.HostName, mq.UserName, mq.Password, mq.VirtualHost);
// 3️. Register Core Publisher (Infrastructure-Level Abstraction)
// - IPublisher is a shared abstraction defined in Messaging.Common.
// - It handles message serialization, setting correlation IDs, and publishing events
// to the RabbitMQ exchange. All microservices rely on this same class for consistency.
builder.Services.AddSingleton<IPublisher, Publisher>();
// 4️. Register Domain-Specific Publisher (ProductService Responsibility)
// - StockReserveEventPublisher implements IStockReserveEventPublisher
// - It knows *which routing keys* to use for stock-related events, such as:
// • stock.reserved → when stock successfully reserved.
// • stock.reservation_failed → when stock reservation fails.
// - These events are published to the OrchestratorService queues.
builder.Services.AddSingleton<IStockReserveEventPublisher, StockReserveEventPublisher>();
// 5️. Register Application Service for Business Logic
// - StockReserveService contains the actual logic that checks inventory,
// validates quantities, and updates the stock table in the database.
// - The Saga pattern calls this service indirectly through the consumer below.
builder.Services.AddScoped<IStockReserveService, StockReserveService>();
// 6️. Register RabbitMQ Consumer for Incoming Saga Event
// - AddStockReserveConsumer() (defined in ProductService.Infrastructure.Messaging.Extensions)
// registers a hosted background service (StockReserveConsumer) that listens to the queue
// bound to the routing key "stock.reservation.requested".
// - When the OrchestratorService publishes a StockReservationRequestedEvent,
// this consumer receives it, processes the stock reservation,
// and then publishes either StockReservedCompletedEvent or StockReservationFailedEvent.
builder.Services.AddStockReserveConsumer();
//Adding JWT Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]!))
};
});
var app = builder.Build();
// TOPOLOGY INITIALIZATION (Ensures Exchange, Queues, and Bindings Exist)
// 7️. During startup, we explicitly ensure that all exchanges, queues, and bindings
// are declared using RabbitTopology.EnsureAll(). This is idempotent — safe to call multiple times.
// - The ProductService uses the same centralized topology structure as other microservices.
// - Ensures this service can immediately start consuming and publishing messages.
using (var scope = app.Services.CreateScope())
{
var ch = scope.ServiceProvider.GetRequiredService<IModel>(); // Active RabbitMQ channel
var opt = scope.ServiceProvider.GetRequiredService<IOptions<RabbitMqOptions>>().Value; // RabbitMQ options
RabbitTopology.EnsureAll(ch, opt); // Declare exchange, queues, bindings, and DLX/DLQ setup
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
Modifying Product Controller to use Authorize Attribute:
We want ProductController to cover all three cases:
- Public (No Token): GetAll, GetById, Search
- Authenticated (Any Valid Token): Create, Update, GetProductByIds
- Admin-only (Role-Based): Delete
Please modify the Product Controller as follows to use the [AllowAnonymous], [Authorize], and [Authorize(Roles = “Admin”)] attributes.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ProductService.API.DTOs;
using ProductService.Application.DTOs;
using ProductService.Application.Interfaces;
namespace ProductService.Api.Controllers
{
[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger<ProductController> _logger;
public ProductController(IProductService productService, ILogger<ProductController> logger)
{
_productService = productService;
_logger = logger;
}
// PUBLIC (AllowAnonymous)
[HttpGet]
[AllowAnonymous] //Authentication not required
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetAll([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
try
{
var products = await _productService.GetAllAsync(pageNumber, pageSize);
return Ok(ApiResponse<List<ProductDTO>>.SuccessResponse(products));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetAll");
return StatusCode(500, ApiResponse<string>.FailResponse("Internal server error"));
}
}
[HttpGet("{id:guid}")]
[AllowAnonymous] //Authentication not required
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetById(Guid id)
{
try
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound(ApiResponse<string>.FailResponse($"Product with id '{id}' not found"));
return Ok(ApiResponse<ProductDTO>.SuccessResponse(product));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetById");
return StatusCode(500, ApiResponse<string>.FailResponse("Internal server error"));
}
}
[HttpGet("search")]
[AllowAnonymous] //Authentication not required
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Search([FromQuery] string? searchTerm, [FromQuery] Guid? categoryId,
[FromQuery] decimal? minPrice, [FromQuery] decimal? maxPrice,
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
try
{
var products = await _productService.SearchAsync(searchTerm, categoryId, minPrice, maxPrice, pageNumber, pageSize);
return Ok(ApiResponse<List<ProductDTO>>.SuccessResponse(products));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Search");
return StatusCode(500, ApiResponse<List<ProductDTO>>.FailResponse("Internal server error."));
}
}
// AUTHENTICATED (Authorize)
[HttpPost]
[Authorize] // any authenticated user
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Create([FromBody] ProductCreateDTO createDto)
{
if (!ModelState.IsValid)
return BadRequest(ApiResponse<string>.FailResponse("Validation failed",
ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList()));
try
{
var result = await _productService.AddAsync(createDto);
if (result == null)
return StatusCode(500, ApiResponse<string>.FailResponse("Failed to create product"));
return Ok(ApiResponse<ProductDTO>.SuccessResponse(result, "Product created successfully"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Create");
return StatusCode(500, ApiResponse<string>.FailResponse("Internal server error"));
}
}
[HttpPut("{id:guid}")]
[Authorize] // any authenticated user
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Update(Guid id, [FromBody] ProductUpdateDTO updateDto)
{
if (!ModelState.IsValid)
return BadRequest(ApiResponse<string>.FailResponse("Validation failed",
ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList()));
if (id != updateDto.Id)
return BadRequest(ApiResponse<string>.FailResponse("ID mismatch"));
try
{
var result = await _productService.UpdateAsync(updateDto);
if (result == null)
return NotFound(ApiResponse<string>.FailResponse($"Product with id '{id}' not found"));
return Ok(ApiResponse<ProductDTO>.SuccessResponse(result, "Product updated successfully"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Update");
return StatusCode(500, ApiResponse<string>.FailResponse("Internal server error"));
}
}
[HttpPost("GetByIds")]
[Authorize] // any authenticated user
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetProductByIds([FromBody] List<Guid> productIds)
{
try
{
if (productIds == null || !productIds.Any())
return BadRequest(ApiResponse<string>.FailResponse("Product IDs list cannot be empty."));
var products = await _productService.GetByIdsAsync(productIds);
return Ok(ApiResponse<List<ProductDTO>>.SuccessResponse(products));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetProductByIds");
return StatusCode(500, ApiResponse<string>.FailResponse("Internal server error"));
}
}
// ADMIN-ONLY (Role-based)
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")] // requires role claim "Admin"
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Delete(Guid id)
{
try
{
await _productService.DeleteAsync(id);
return Ok(ApiResponse<string>.SuccessResponse(string.Empty, "Product deleted successfully"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Delete");
return StatusCode(500, ApiResponse<string>.FailResponse("Internal server error"));
}
}
}
}
Testing the Features:
Login (Normal User)
Method: Post
URL: {{gateway_base}}/users/user/login
Headers: Content-Type: application/json
Body:
{
"EmailOrUserName": "pranayakumar777@gmail.com",
"Password": "Test@12345",
"ClientId": "web"
}
Public Endpoints (No Token Needed)
GetAll
- GET {{gateway_base}}/products/products?pageNumber=1&pageSize=20
GetById
- GET {{gateway_base}}/products/products/E2F3D4C5-7A8B-4B23-8D9E-3C456789ABCD
Authenticated Endpoints (Require Authorization: Bearer {{access_token}})
Create
Method: Post
URL: {{gateway_base}}products/products
Headers:
- Content-Type: application/json
- Authorization: Bearer {{access_token}}
Body:
{
"Name": "Sample Phone X",
"Description": "Great phone for testing",
"Price": 499.99,
"StockQuantity": 50,
"IsActive": true,
"CategoryId": "D7F8E3C4-6A7B-4A12-9C9F-2B3456789ABC"
}
Update
Method: Put
URL: {{gateway_base}}/products/products/bd8427d7-f886-45b2-8559-7b8c278336be
Headers:
- Content-Type: application/json
- Authorization: Bearer {{access_token}}
Body:
{
"Id": "bd8427d7-f886-45b2-8559-7b8c278336be",
"Name": "Sample Phone X",
"Description": "Great phone for testing",
"Price": 529.99,
"StockQuantity": 60,
"IsActive": true,
"CategoryId": "D7F8E3C4-6A7B-4A12-9C9F-2B3456789ABC"
}
Admin-only Endpoint (Require Authorization: Bearer {{admin_access_token}})
Delete
Method: DELETE
URL: {{gateway_base}}/products/products/bd8427d7-f886-45b2-8559-7b8c278336be
Headers:
- Authorization: Bearer {{admin_access_token}}
Conclusion:
Implementing JWT Authentication in the Ocelot API Gateway ensures that every request entering our microservices ecosystem is authenticated and secure from the very edge. The gateway validates incoming tokens, filters out invalid or expired ones, and allows only legitimate traffic to reach our internal services. This approach creates a clean separation of responsibilities, authentication at the Gateway, and authorization within each microservice, resulting in a scalable, secure, and maintainable architecture.
This design also delivers three key benefits:
- Security at the Edge: Blocks tampered or unauthorized requests before they reach internal systems.
- Consistency Across Services: All incoming traffic follows a unified authentication process.
- Autonomy and Flexibility: Each microservice still controls its own access rules and role-based authorization.
This architecture aligns with industry standards for large-scale systems (such as Netflix, AWS, and Microsoft), offering a balance between Performance, Security, and Maintainability.

