Microservices with IdentityServer4 and Ocelot Fronting a .NET Core API

Well just like the title says I want to show a complete microservice-based architecture using the lightweight IdentityServer4 for authentication and Ocelot as an API gateway. Ocelot will act as a reverse proxy for a secured internal ASP.NET Core Web API. Everything here is open-source .NET Core 2.0 or later.

The main source of guidance I consulted for this architecture is the eShopContainers project and the white paper they published (which I read cover-to-cover at my favorite coffee shop and I recommend you do the same). There are a few helpful blog posts out there too. Dan Patrascu-Baba wrote a couple posts (here and here), Scott Brady wrote a helpful intro to IdentityServer4, and Catcher Wong wrote a nice series on Ocelot. But I couldn’t find a “complete picture” presentation of the whole architecture so I decided to write it myself. My goal here is to present a bare bones framework in one place to help bootstrap a serious microservices project.

I’ve organized this post into three parts: (1) The Big Picture; (2) The Configuration; and (3) The Deep Dive. Let’s get started right after the jump…

The Big Picture

Our client app will pass credentials to an Identity Server and receive back a JSON Web Token (JWT). Maybe in your project you want to use OpenID Connect or some other OAuth 2 protocol but here I’m thinking of a server-to-server scenario where the client might be a backend for a frontend. So we’re going with simple client credentials. Once the client has a bearer token it will call the API endpoint which is fronted by Ocelot. I won’t dive into everything Ocelot can do in this post. I’m just using it here as a reverse proxy. But just for fun I will configure rate limits to protect our internal API from a DDoS attack. After Ocelot reroutes the request to the internal API, it will present the token to Identity Server in the authorization pipeline. If the client is authorized the request will be processed and a list of widgets will be sent back to the client. Here’s the big picture:

The Configuration

The source is checked into GitHub here: https://github.com/jamesstill/MicroserviceExample. Clone it and follow these next steps if you want to configure this to run on your own machine. Also make sure you have the latest updates in Visual Studio 2017.

Set up an App Secret

In this solution Identity Server uses an RSA key rather than an X.509 certificate. Several online examples use a cert and that’s fine if you’re hosting on-prem with a machine certificate store. But if you want to host this in Azure then that option seems to be out. Azure Key Vault no longer supports storing a certificate as a secret. And I wasn’t able to import a self-signed cert into the key vault because it wasn’t signed by a CA. Generating a cert automatically within the key vault does no good either because the private key is hidden from you. IdentityServer4 needs that private key to sign the tokens it issues. So in the end I opted to go with an RSA key. [Update 2 Apr 2019: Yes you can use an X509 cert with an Azure App Service! See here and here for two excellent write ups on how to do it.]

But where to put that key? In production we would use the key vault. In development, I’m using Secret Manager to store the key and keep it out of my solution. That way I won’t accidentally check it into source control. So go into the folder called RSAKeys. You’ll see I used the generate.bat file to create a public/private key pair using OpenSSL. Use the keys I generated or if you’re on Windows 10 you can download the binaries that Shining Light makes available and create new keys. Superdry Developer has a web page where you can convert the PEM format to XML so do that too.

Now right-click on the IdentityServer project in Visual Studio and select Manage User Secrets. The secrets.json file is stubbed out for you. Take the RSA parameters of the private key and put them into the secrets.json file so we can later map the object literal to a POCO named KeyParameters in the project. It should look like this:

{
 "KeyParameters:D": "value from XML goes here",
 "KeyParameters:DP": "value from XML goes here",
 "KeyParameters:DQ": "value from XML goes here",
 "KeyParameters:Exponent": "value from XML goes here",
 "KeyParameters:InverseQ": "value from XML goes here",
 "KeyParameters:Modulus": "value from XML goes here",
 "KeyParameters:P": "value from XML goes here",
 "KeyParameters:Q": "value from XML goes here"
}

Save and close the file.  

Configure Port Numbers

For each of the three projects Gateway, IdentityServer, and WidgetApi open their properties window (Alt + ENTER) and select the Debug tab. Uncheck Launch browser and check Enable SSL. Jot down the SSL port numbers for each of them. Update these four locations in code with the correct port number from your build. The first port is in Gateway in the ocelot.json configuration file:

ocelot.json line 9
ocelot.json line 9

The second port is in WidgetApi in the Startup.cs file:

Startup.cs line 20
Startup.cs line 20

And the last two are in TestClient in the Program.cs file:

Program.cs
Program.cs line 25

 

Program.cs line 49
Program.cs line 49

Obviously in production these values would live in an appsettings.json file or in the CI/CD build pipeline but for this example they are hard-coded.

Configure Startup Projects

Click on the solution name in Solution Explorer and choose Projects > Set Startup Projects. Choose multiple startup projects and set the Action to Start for Gateway, IdentityServer, and WidgetApi:

  

Apply and close. Configuration is done!

Do a build and we’re ready to smoke test the solution. Click Start (or F5) and once they are up and running right-click on the TestClient and choose Debug > Start New Instance. You should see output like this:

If you get an error double check the first-time configuration settings above. Now change the password on line 27 to “foobar” and run it again. This time authentication should fail:

Congratulations! You’ve exercised the system end to end. Now let’s dive into this thing to understand what’s going on.

The Deep Dive

IdentityServer4

By now you’ve read the eShopContainers eBook and you’ve reviewed the IdentityServer4 (IS4) documentation.  So you know that IS4 is a framework that provides centralized authentication, authorization, and claims management for your clients and microservices. In this example we want to use IS4 to issue an access token to our client who must then present that token to the API.  All you have to do is install the IdentityServer4 nuget package in your .NET Core 2 project using the Empty template.

The client credentials and its claim is hard-coded in the Config.cs file:

public class Config
{
    private const string ClientUsername = "WidgetClient";
    private const string ClientPassword = "p@ssw0rd";
    private const string ClientResource = "widgetapi";

    // scopes define the API resources in your system
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource(ClientResource, "WidgetApi")
        };
    }

    // clients want to access resources (aka scopes)
    public static IEnumerable<Client> GetClients()
    {
        // client credentials client
        return new List<Client>
        {
            new Client
            {
                ClientId = ClientUsername,
                AllowedGrantTypes = GrantTypes.ClientCredentials,

                ClientSecrets =
                {
                    new Secret(ClientPassword.Sha256())
                },
                AllowedScopes = { ClientResource }
            }
        };
    }
}

In a production app you can use EF Core to store these in a database (or old skool ADO.NET if you don’t like EF). I’ll leave it to you to implement that in your application. IS4 provides an AdminUI tool to make user management easier. I have not used it so I can’t say more about it.

Notice the KeyParameters.cs POCO:

public class KeyParameters
{
    public string D { get; set; }
    public string DP { get; set; }
    public string DQ { get; set; }
    public string Exponent { get; set; }
    public string InverseQ { get; set; }
    public string Modulus { get; set; }
    public string P { get; set; }
    public string Q { get; set; }
}

In Startup.cs this POCO is constructed in the GetRSAParameters method. Then it is used to return a System.Security.Cryptography.RSAParameters struct. Why not just construct it directly? Well the RSAParameters struct isn’t serializable so the POCO is a wrapper to work around it. In any case beginning with ASP.NET Core 2 AddUserSecrets is automatically called by CreateDefaultBuilder when you’re in the Development environment. So GetRSAParameters just has to pull it out of the Configuration section.

That’s all there is to it really. Let’s look at the API briefly.

WidgetApi

The API has a single controller with a single operation and represents nothing more than a bare minimum microservice for illustration purposes:

[Authorize]
[Route("api/v1/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        await Task.Delay(100); // simulate latency
        var items = new List<Widget>()
        {
            new Widget { ID = 1, Name = "Cog", Shape = "Square" },
            new Widget { ID = 2, Name = "Gear", Shape = "Round" },
            new Widget { ID = 3, Name = "Sprocket", Shape = "Octagonal" },
            new Widget { ID = 4, Name = "Pinion", Shape = "Triangular" }
        };

        return Ok(items);
    }
}

Notice the Authorize attribute protecting the controller. Authorization will be wired up through the AccessTokenValidation nuget package:

AccessTokenValidation

This is configured in Startup.cs by telling the authorization handler to validate the bearer token from the client against the IS4 server:

public void ConfigureServices(IServiceCollection services)
{
    var authenticationUrl = "https://localhost:44386";

    services.AddMvcCore()
        .AddAuthorization()
        .AddJsonFormatters();

    services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options =>
        {
            options.Authority = authenticationUrl;
            options.RequireHttpsMetadata = true;
            options.ApiName = "widgetapi";
        });
}

As you can see it’s pretty much the same as any other authorization middleware you might use in the pipeline.  And of course you have to tell the API to use authentication in the app:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseMvc();
}

Believe it or not that’s it. The API will present the bearer token to IS4 and if it is valid and the bearer has a claim (authorization) to use this resource then the request is approved and the widgets are sent back in response. If not, an HTTP 401 is returned.

Gateway

Like the IS4 project the API Gateway is just a .NET Core 2 project using the Empty template. Following eShopContainers I used the lightweight Ocelot as a reverse proxy to protect the internal API from direct calls. After adding the Ocelot nuget package I just had to configure the app to use the library:

public static void Main(string[] args)
{
    new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config
                .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
                .AddJsonFile("ocelot.json")
                .AddEnvironmentVariables();
        })
        .ConfigureServices(s =>
        {
            s.AddOcelot();
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            //add your logging
        })
        .UseIISIntegration()
        .Configure(app =>
        {
            app.UseOcelot().Wait();
        })
        .Build()
        .Run();
}

Notice we’re injecting this ocelot.json file which lives in the project root:

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/v1/widget",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 44325
        }
      ],
      "UpstreamPathTemplate": "/api/v1/widget",
      "UpstreamHttpMethod": [ "Get" ],
      "RateLimitOptions": {
        "ClientWhitelist": [],
        "EnableRateLimiting": true,
        "Period": "1s",
        "PeriodTimespan": 1,
        "Limit": 1
      }
    }
  ]
}

The Ocelot documentation covers this very well so I won’t repeat that here.  I’ll just point out that I’ve configured this as a simple pass through so that Ocelot does not attempt to authenticate the client before rerouting the request downstream to WidgetApi. That is an option if you want to do so and I hope to cover Ocelot in more detail in a future blog post.

As you can see any client calling the gateway with a path containing /api/v1/widget will be rerouted to our internal WidgetApi on the same path. This is as basic as it gets. But Ocelot supports query string parameters and more complicated rerouting scenarios. 

I have also configured a basic rate limit so that upstream clients cannot overload our API with more than one request per second. 

Client Console App

The client is a .NET Core console app with the IdentityModel nuget package.

With the IdentityModel client library installed I can now call the IS4 discovery endpoint to retrieve all necessary metadata to authenticate and receive a token:

var discoveryUrl = "https://localhost:44386/";
var discoveryClient = new DiscoveryClient(discoveryUrl);
var discoveryResponse = await discoveryClient.GetAsync();
if (discoveryResponse.IsError)
{
    throw new Exception("Failed to get discovery response!");
}

I can cache the discovery document for future calls to the server. But if server keys are being rotated at some interval the client would have to refresh the cache. I’ll go into this more in a future blog post. Now that I have the discovery I can pass in the client credentials to get back a token response:

var clientId = "WidgetClient";
var clientSecret = "p@ssw0rd";
var scope = "widgetapi";
var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, clientId, clientSecret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync(scope);
if (tokenResponse.IsError)
{
    throw new Exception("Authentication failed!");
}

If my token response is not in error then I can set that token in the request header and call Ocelot to fetch the list of widgets. As we saw earlier Ocelot will reroute the request downstream to our internal WidgetApi where it will validate the token.

var uri = "https://localhost:44336/api/v1/widget";
var gatewayClient = new HttpClient();
gatewayClient.SetBearerToken(tokenResponse.AccessToken);
var response = await gatewayClient.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
    Console.WriteLine("Received HTTP 200 from API. Writing widgets to console...");
    Console.WriteLine(Environment.NewLine);

    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine(content);
}
else
{
    Console.WriteLine("Response was unsuccessful with status code: " + response.StatusCode);
}

Conclusion

That’s the bare bones setup for a microservices architecture using IdentityServer4 and Ocelot as an API gateway to proxy the internal array of microservices. In this example we just had the one WidgetApi but there could be a dozen internal APIs for a production application. 

Next Steps

Rather than simple client credential authentication an MVC web application client or mobile app could use OpenID Connect (which is an extension of the OAuth2 protocol). All of the support for this is already in IdentityServer4. We would just need to wire it up client side. The folks at IdentityServer provided a Quickstart UI to see how that would be done.

I just scratched the service with Ocelot. It can support load balancing, service discovery,  caching, and you can inject your own middleware in the pipeline to handle any scenario during the pre and post downstream request lifecycle. 

Last I don’t like how I handled the RSA private key. Maybe the user secret should have been the full XML file rather than a POCO. I might revisit this in the future but leave a comment if you know a better way.

I’ll be sure to update this post if I explore any and all of these options further.