Microservices
.NET Core ASP.NET Core Containers Microservice Patterns Uncategorized Visual Studio Web API

How to Integrate Authentication into a Microservice API Gateway

Welcome to today’s blog.

In the previous post I showed how to implement a basic API gateway with URL routing of upstream API requests to downstream API services.

Today I will be showing how you can use .NET Core to extend our API Gateway with authentication.

How do we implement authentication in our API Gateway?

Instead of permitting a client to connect directly to one of our downstream microservice API services, we can provide another layer of authentication from the gateway, accessible from the upstream API. Compared to the direct API service call, there would be almost no difference in the way we call the same API service through the gateway. We would still have to pass a JSON payload through a POST and when requesting protected API methods, we would still need to pass a valid token to gain access.

Our identity service can either be one of our downstream API microservices, it can be hosted on a separate server, or it can be a third-party external identity provider. All it has to provide is a valid token which we can use to make a HTTP request to our downstream APIs through our upstream gateway API.

In previous posts I showed how to use a custom identity server to authenticate a Web API service using JWT security. In this post I will add an API gateway and utilise an external identity server to route to API methods using JWT tokens. This will give us one more secure point of entry for all our Web API service endpoints.

A diagram depicting our architecture is shown below:

API gateway authentication

In the implementation discussion I will assume that you have already implemented two basic Web API services that will be routed within our API Gateway. In the previous post I showed how to implement a basic API gateway using .NET Core and configure the upstream and downstream API microservice routing using ocelot.json

If our Web API service has already implemented support for validation of JWT tokens at the method (declarative) level, then the API implementation does not need any further changes to accommodate forwarding of JWT security from the API Gateway. 

Routing of Authenticated Upstream Requests

To be able to route authenticated requests we require the three dependencies:

  1. An identity provider API, either custom or third-party service that will issue a valid JWT token.
  2. A downstream API method that has the [Authorize] attribute.
  3. Configuration of the identity provider API service using .NET Core ASP.NET Identity middleware in our gateway start up.

We have simplified our gateway somewhat by not producing an issuer or authority in our token, so we can obtain a valid token from a custom JWT authorization service. This can be done using POSTMAN or CURL.

After obtaining the token, we can construct a HTTP request to our upstream API gateway using POSTMAN.

After adding JWT token validation support to our API Gateway, we can then submit an authenticated HTTP request to the gateway using our generated JWT Bearer token.  

First, I will show how to amend ConfigureService()  in our Gateway .NET Core API service to support the validation of JWT tokens in upstream requests. In ConfigureServices() we include the following namespaces:

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

Then use the AddAuthentication() middleware extension with JWT Bearer authentication scheme and validate the token using our signing key which consists of our application secret. As we discussed earlier, we will not be validating the authority or issuer in this basic scenario.

public void ConfigureServices(IServiceCollection services)
{
    var authenticationProviderKey = "IdentityApiKey";

    var key = Convert.FromBase64String(
Configuration.GetSection("AppSettings:Secret").Value);

    services.AddAuthentication(a =>
    {
        a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(authenticationProviderKey, x =>
    {
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });

    services.AddOcelot();
}

In our gateway configuration file ocelot.json, we will need to let the upstream handlers accept the authentication token. To do this, we add the AuthenticationOptions key with the AuthenticationProviderKey value we declared within the JWT bearer middleware configuration.

{
    "Routes": [
    {
        "DownstreamPathTemplate": "/bookloan.catalog.api/api/book/{everything}",
        "DownstreamScheme": "http",
        "DownstreamHostAndPorts": [
        {
                "Host": "localhost",
                "Port": 80
        }
        ],
        "UpstreamPathTemplate": "/book/{everything}", 
        "UpstreamHttpMethod": [ "POST", "PUT", "GET" ],
        "AuthenticationOptions": {
            "AuthenticationProviderKey": "IdentityApiKey",
            "AllowedScopes": []
        }
    },
    {
        "DownstreamPathTemplate": "/bookloan.loan.api/api/loan/{everything}",
        "DownstreamScheme": "http",
        "DownstreamHostAndPorts": [
        {
            "Host": "localhost",
            "Port": 80
        }
        ],
        "UpstreamPathTemplate": "/loan/{everything}",
        "UpstreamHttpMethod": [ "POST", "PUT", "GET" ],
        "AuthenticationOptions": {
            "AuthenticationProviderKey": "IdentityApiKey",
            "AllowedScopes": []
        }
    }
    ],
    "GlobalConfiguration": {
        "BaseUrl": "http://localhost:5000",
        "RequestIdKey": "OcRequestId",
        "AdministrationPath": "/administration"
    }
}

After this configuration, we are ready to build, run and test our API gateway authentication.

We are ready to submit a POSTMAN request to our upstream gateway API and test the authentication of our JWT token.

We execute the upstream API method http://localhost:5000/book/list which requires a valid JWT token to execute. The equivalent downstream API method http://localhost/BookLoan.Catalog.API/api/Book/AllBooks will be called. If successful, it will return data as a JSON array response with status 200 (OK).

In the API gateway console output, you will first see the upstream URL matching the equivalent downstream URL:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5000/book/list
dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: ocelot pipeline started
dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: Upstream url path is /book/list
dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: downstream templates are /bookloan.catalog.api/api/book/{everything}

Then we see the authorisation middleware complete validation of the token and client:

info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: /book/list is an authenticated route. AuthenticationMiddleware checking if client is authenticated
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[2]
      Successfully validated the token.
info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: Client has been authenticated for /book/list

The authorisation middleware then checks if the route is authenticated, the scopes are checked, and user scopes are authorized:

info: Ocelot.Authorisation.Middleware.AuthorisationMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: route is authenticated scopes must be checked
info: Ocelot.Authorisation.Middleware.AuthorisationMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: user scopes is authorised calling next authorisation checks
info: Ocelot.Authorisation.Middleware.AuthorisationMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: /bookloan.catalog.api/api/book/{everything} route does not require user to be authorised

Finally, the authenticated downstream URL is called with a HTTP request, with a JSON response:

dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: Downstream url is http://localhost/bookloan.catalog.api/api/book/list
info: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: 200 (OK) status code, request uri: http://localhost/bookloan.catalog.api/api/book/list
dbug: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: setting http response message
dbug: Ocelot.Responder.Middleware.ResponderMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: no pipeline errors, setting and returning completed response
dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]
      requestId: 0HM6D19SAI0VH:00000001, previousRequestId: no previous request id, message: ocelot pipeline finished
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 22692.8991ms 200 application/json; charset=utf-8

If we wait sometime and then re-attempt the submission from POSTMAN, the response from the gateway API will be an unauthorized 401 error as shown in the gateway console log:

nfo: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[1]
      Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden. For more details, see 
…
      IdentityApiKey was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]', Current time: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
warn: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0]
      requestId: 0HM6D19SAI0VI:00000001, previousRequestId: no previous request id, message: Client has NOT been authenticated for /book/list and pipeline error set. Request for authenticated route /book/list by  was unauthenticated
…
dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0]
      requestId: 0HM6D19SAI0VI:00000001, previousRequestId: no previous request id, message: ocelot pipeline finished
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 2508.0395ms 401

We have managed to successfully implement and test authentication of our API gateway.

As I mentioned, this is still a basic configuration with some assumptions. In a later post I will show how to use a public and private key for our token validation signing key and how to use the authority and issuer in generated tokens to make our gateway token validation more secure. In addition, I will explore how to apply authorization middleware to check user roles within tokens to determine routing on a more granular level.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial