Microservices are a common design pattern nowadays in software applications. Moreover, we usually have some sort of API Gateway, such as Ocelot, to provide a single API for consumers of our microservices and encapsulate underlying implementation details and handle authentication, which is often implemented with JWT.

In this article, we are going to explore how we secure our microservices behind the Ocelot API Gateway by using JWT authentication.

Note – we aren’t going to cover the finer details of setting up JWT authentication or an Ocelot API Gateway. We recommend you check out our great articles on JWT Authentication and Ocelot API Gateway before diving into this one.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
To download the source code for the video, visit our Patreon page (YouTube Patron tier).

With that, let’s start.


VIDEO: Secure Microservices Using JWT With Ocelot in .NET Core.


Create Microservice

Before we look at setting up JWT authentication with Ocelot, we need a microservice to validate that our authentication works as expected. To start, let’s create a new ASP.NET Core Web API using the Visual Studio Project wizard or the dotnet new webapi command.

Our microservice’s domain will be shoes, so let’s create a basic model for this:

public class Shoe
{
    public required int Id { get; set; }

    public required string Name { get; set; }

    public required string Brand { get; set; }

    public decimal Price { get; set; }
}

Here we define some required members for our model.

Next, we’ll create a repository to allow us to interact with our shoes:

public class ShoeRepository : IShoeRepository
{
    private static List<Shoe> _shoes = new()
    {
        new()
        {
            Id = 1,
            Name = "Pegasus 39",
            Brand = "Nike",
            Price = 119.99M
        },
        new()
        {
            Id = 2,
            Name = "Vaporfly",
            Brand = "Nike",
            Price = 229.99M
        },
        new()
        {
            Id = 3,
            Name = "Ride 15",
            Brand = "Saucony",
            Price = 119.99M
        }
    };

    public List<Shoe> GetShoes() => _shoes;

    public bool DeleteShoe(int id)
    {
        var shoe = _shoes.FirstOrDefault(s => s.Id == id);

        if (shoe is not null)
        {
            return _shoes.Remove(shoe);
        }

        return false;
    }
}

For the purposes of this article, we’ll simply store our shoes in memory as a list.

Here, we implement the IShoeRepository interface, which has two methods defined, GetShoes() and DeleteShoe(). These methods will allow us to test out our authentication later on.

We need to register this interface with the dependency injection framework in the Program class:

var builder = WebApplication.CreateBuilder(args);

// code removed for brevity

builder.Services.AddScoped<IShoeRepository, ShoeRepository>();

var app = builder.Build();

// code removed for brevity 

app.Run();

Finally, we’ll expose an API for our microservice so we can interact with it over HTTP:

[Route("api/[controller]")]
[ApiController]
public class ShoesController : ControllerBase
{
    private readonly IShoeRepository _shoeRepository;

    public ShoesController(IShoeRepository shoeRepository)
    {
        _shoeRepository = shoeRepository;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(_shoeRepository.GetShoes());
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        var shoeDeleted = _shoeRepository.DeleteShoe(id);

        return shoeDeleted ? NoContent() : NotFound();
    }
}

First, we create a private property for our IShoeRepository interface, which we inject into the class constructor.

Then, we define two API methods to reflect the methods exposed from our repository class.

Before we configure Ocelot to use JWT authentication, let’s verify we can communicate with our Shoe microservice through Ocelot.

Route Microservice Through Ocelot

In our Ocelot project, let’s add the routes for the Shoe microservice to the ocelot.json configuration file:

{
  "Routes": [
    {
      "UpstreamPathTemplate": "/shoes",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/shoes",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/shoes/{id}",
      "UpstreamHttpMethod": [ "Delete" ],
      "DownstreamPathTemplate": "/api/shoes/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:5000"
  }
}

Here, we define two separate routes. One for our GET method, and one for our DELETE method in our Shoe microservice.

Finally, we must remember to include the BaseUrl for our Ocelot gateway, which will be the same URL as what is configured in our launchSettings.json file.

We haven’t gone through the setup of the Ocelot project in this article, as we have articles that cover that in great detail. Please refer to the source code for this article to see the basic configuration (link directly to the Ocelot project).

Now that we have our microservice created and our Ocelot gateway configured, let’s run both applications and make a request to https://localhost:5000/shoes which will return our list of shoes through our Ocelot gateway. Also, we can confirm our delete method works by making a delete request to /shoes/1 to delete the shoe with ID 1.

Great! We have our gateway configured correctly to handle requests to our Shoe microservice. Next, let’s explore how we add JWT authentication to our application.

Secure Ocelot With JWT Authentication

Ocelot provides various ways to configure authentication, with JWT being one of them. So, let’s start to incorporate JWT authentication into our gateway and microservice combination.

To start, we need a way to generate JWT tokens. We don’t want to store this logic in our gateway or microservice, so we’ll create a separate API project for authentication, again using the Visual Studio Project wizard or the dotnet new webapi command.

Configure JWT Authentication

With our project created, let’s create a record to model a user:

public record User(string Username, string Password, string Role, string[] Scopes);

Here, we have a very simple user model with a UsernamePassword ,Role, and Scope property. The last two properties will be used when we look at advanced authorization for Ocelot.

Now, let’s create a service to generate our JWT:

public class JwtTokenService
{
    private readonly List<User> _users = new()
    {
        new("admin", "aDm1n", "Administrator", new[] {"shoes.read"}),
        new("user01", "u$3r01", "User", new[] {"shoes.read"})
    };

    public AuthenticationToken? GenerateAuthToken(LoginModel loginModel)
    {
        var user = _users.FirstOrDefault(u => u.Username == loginModel.Username 
                                           && u.Password == loginModel.Password);

        if (user is null)
        {
            return null;
        }

        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtExtensions.SecurityKey));
        var signingCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
        var expirationTimeStamp = DateTime.Now.AddMinutes(5);

        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Name, user.Username),
            new Claim("role", user.Role),
            new Claim("scope", string.Join(" ", user.Scopes)) 
        };

        var tokenOptions = new JwtSecurityToken(
            issuer: "https://localhost:5002",
            claims: claims,
            expires: expirationTimeStamp,
            signingCredentials: signingCredentials
        );

        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);

        return new AuthenticationToken(tokenString, (int)expirationTimeStamp.Subtract(DateTime.Now).TotalSeconds);
    }
}

We won’t go into depth in this article on all the configurations for JWT. Check out the previously linked article if you’d like to learn more about this.

Saying that there are a couple of things worth mentioning. First is our list of users which we’ll use to validate if a login attempt is valid.

Secondly, we add the Role and Scope properties of the user to the claims collection, and we’ll see that in use later.

Finally, we set the issuer to https://localhost:5002 which is the port our Authentication API will be listening on.

We can now use this token service to create our Authentication API controller:

public class AuthController : ControllerBase
{
    private readonly JwtTokenService _jwtTokenService;

    public AuthController(JwtTokenService jwtTokenService)
    {
        _jwtTokenService = jwtTokenService;
    }

    [HttpPost]
    public IActionResult Login([FromBody] LoginModel user)
    {
        var loginResult = _jwtTokenService.GenerateAuthToken(user);

        return loginResult is null ? Unauthorized() : Ok(loginResult);
    }
}

Here, we have a single POST endpoint for login attempts. If the login attempt is unsuccessful, we return 401 Unauthorized otherwise we return the valid JWT.

With our Authentication API created, next, we’ll create a class library as we’ll share the registration code between our gateway and microservice. Within this class library, let’s create an extension method:

public static class JwtExtensions
{
    public const string SecurityKey = "secretJWTsigningKey@123";

    public static void AddJwtAuthentication(this IServiceCollection services)
    {
        services.AddAuthentication(opt => {
            opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = "https://localhost:5002",
                ValidateAudience = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey))
            };
        });
    }
}

Here, we configure the JWT authentication scheme. In the AddJwtBearer() method, we set our signing key and ensure we use our Authentication API as the ValidIssuer.

Now we can use this extension method in our microservice and Ocelot gateway in the Program class:

var builder = WebApplication.CreateBuilder(args);

// code removed for brevity

builder.Services.AddJwtAuthentication();

var app = builder.Build();

// code removed for brevity

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

app.Run();

First, we call the AddJwtAuthentication() method as part of the service registration. Also, we call the UseAuthentication() and UseAuthorization() methods to ensure our applications correctly use the JWT authentication scheme.

Protect Microservice Through Ocelot With JWT Authentication

The final thing we need to do to configure Ocelot to secure our microservice is to set up the authentication in ocelot.json:

{
  "Routes": [
    {
      "UpstreamPathTemplate": "/auth",
      "UpstreamHttpMethod": [ "Post" ],
      "DownstreamScheme": "https",
      "DownstreamPathTemplate": "/api/auth",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ]
    },
    // code removed for brevity
    {
      "UpstreamPathTemplate": "/shoes/{id}",
      "UpstreamHttpMethod": [ "Delete" ],
      "DownstreamPathTemplate": "/api/shoes/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer"
      }
    }
  ]
}

First, we add our Authentication API from earlier, so requests are routed through Ocelot.

Next, we only want to protect our delete endpoint from unauthorized access, so we add the AuthenticationOptions section to our delete route. In this section, we define the provider key as Bearer. This tells Ocelot to check for the Bearer authentication scheme by validating if the user is authorized.

Let’s run our application and make a request to /shoes which will still return our list of shoes as expected. Next, let’s try to make an unauthenticated request to /shoes/1. This time it will fail with a 401 Unauthorized status code, which proves our JWT authentication is working as expected and securing our microservice.

Now, let’s retrieve a JWT from our Authentication API by making a request to /auth, providing a valid user in the body:

{ "username": "admin", "password": "aDm1n" }

Our API will return a valid token which we can now use in our delete request, ensuring we pass it in the Authorization header with the Bearer token type. This time, we receive a 204 NoContent meaning we have successfully deleted a shoe. We can confirm this by making another request to /shoes where we’ll only retrieve 2 shoes instead of 3.

Excellent! We’ve secured our microservice that sits behind an Ocelot API gateway with JWT authentication. Next, let’s look at some of the more advanced options we have for securing our microservices with Ocelot.

JWT Scope-Based Authorization With Ocelot

Ocelot allows us to secure our endpoints using scopes, checking if the authenticated request has the correct scope before sending the request to our microservice.

When we generate our JWT, we add the scope claim, so let’s use this to protect our GET endpoint in ocelot.json:

{
  "Routes": [
   // code removed for brevity 
    {
      "UpstreamPathTemplate": "/shoes",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/shoes",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": ["shoes.read"]
      }
    }
    // code removed for brevity 
  ]
}

Here, we add the AuthenticationOptions section to our GET endpoint, once again requiring the Bearer provider key. This time, we add the AllowedScopes key, which accepts an array of scopes. We use our shoes.read scope, which both our users have assigned.

Let’s test this out by generating a JWT with one of our users, and making a request to /shoes which will return our list of shoes as expected.

If the user doesn’t have the correct scope assigned, Ocelot returns a 403 Forbidden.

With this, we can secure multiple endpoints and restrict access to users with the correct scopes only.

JWT Role-Based Authorization With Ocelot

Ocelot provides claims-based authorization which we can use to further protect our microservices.

When we defined our User model, we provided a Role property, which we can use to restrict access to certain endpoints in our microservice. Let’s set up a restriction so that only users with the Administrator role can delete shoes:

{
  "Routes": [
    // code removed for brevity
    {
      "UpstreamPathTemplate": "/shoes/{id}",
      "UpstreamHttpMethod": [ "Delete" ],
      "DownstreamPathTemplate": "/api/shoes/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer"
      },
      "RouteClaimsRequirement": {
        "role": "Administrator"
      }
    }
  ]
}

Here, we add the RouteClaimsRequirement section, providing the role key with the Administrator value. This tells Ocelot to check each request for the role claim in the JWT, and only allow requests with the Administrator value to pass through.

Let’s put this theory to the test. Running our application, this time we’ll log in as a user with the User role:

{ "username": "user01", "password": "u$3r01" }

As we haven’t restricted access to the GET endpoint, we can still make requests to /shoes and retrieve our list of shoes. However, when we make a delete request to /shoes/1, we receive a 403 Forbidden.

Now let’s generate a JWT as an administrator and make the same request. This time we receive a 204 NoContent as expected, and our shoe with ID 1 is deleted successfully.

Conclusion

Securing our microservices is vitally important if we don’t want unauthorized users to access sensitive data. In this article, we looked at how to secure microservices that use the Ocelot Gateway with JWT authentication. First, we saw how to restrict all access if the user is unauthenticated, and then explored scoped-based and role-based authorization and how we configure Ocelot to handle this for us.

This keeps authorization logic out of our microservice and allows us to store the authorization configuration in a single, easy-to-manage location.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!