API Management – OAuth and private back-ends


Azure API Management is a fully managed API Gateway service.

In my last article we looked at the anatomy of the service. Today I wanted to demonstrate how to use OAuth with JWT token to protect an API Front End. I also wanted to show how we can access backend APIs on private networks.

There are a few other interesting bits:

  • How to transform a API with HTTP-GET & query string to an HTTP-POST & JSON body
  • How to have an API supporting OAuth/JWT when access through a product and just plain-subscription when access through another
  • How to implement a simple screen-scrap within API Management
  • How to lock down Logic App to only accept connections from API Management

Probably the biggest contribution of this article is to show how to do this all with an ARM template. ARM template doesn’t only allow to quickly build a demo environment, but it enables Dev-Ops. API Management has a notoriously complicated ARM model, but with time and patience and the help of this DevOps resource kit, you too can make it.

Why would we want to use OAuth / JWT to protect our API? By default, an API in API Management is protected using a subscription key. This is good but it can be leaked. A JWT token is short lived and hence is a little stronger. If we use Managed Service Identity (MSI) in consumer, it is trivial to acquire a JWT and impossible to leak the secret (certificate) of the MSI. So the solution becomes much more secure.

As usual, the code is in GitHub.

Sample solution

Here is the solution we are going to build:

Sample Solution

We have an API Management service in the middle. It is integrated to a VNET in external mode. Since it is part of a VNET, it can communicate with other VNET-bound service. The diagram shows a private IP ; it isn’t a service private VIP as it is deployed in external mode. Rather this represents the private IP of whatever VM happened to communicate with a private service.

The private service is implemented using an Azure Container Instance for pure convenience: it is quick and simple to deploy.

The public service is implemented with a Logic App, because it’s simple to implement and monitor.

The public consumer also is implemented with a Logic App for similar reasons.

We can easily deploy the solution:

Deploy button

The ARM template has 4 parameters:

Parameter Mandatory Description
organizationName X Name of the organisation for API Management. This is also used for the name of the service.
adminEmail X Email for the admin
tenantName X Name of the Azure AD tenant (related to following parameter)
appId X Azure AD Application ID ; this application is used to authenticate API users

As usual, to author the ARM template, we looked at the online documentation, reverse engineered a deployment made from the portal, checked out some Azure Quickstart Templates and use our imagination.

Resources

Once we’ve deployed the ARM Template, we can take a quick look at the resources in the Portal:

Virtual Network

We can see we have 2 subnets. One for the API Management VMs and one for the private service. The latter is delegated to the Azure Container Instance (ACI)service. This is required to deploy an ACI inside a subnet.

subnets

Azure Container Instance

Nothing much to say about the private service. We can see the container image is from Docker Hub:

Container image

We are basically using the same image we’ve built in the Docker Getting Started which is basically a Python Flask Hello World.

This is the service we are going to screen scrap.

public-consumer-subscription

This service calls an API Management front end using a subscription key.

public-consumer-token

This service calls an API Management front end using a subscription key and a JWT token.

This is done using a Managed Service Identity (MSI) in Logic App.

public-service

This is the public service. It’s a simple HTTP-post service expecting a JSON body.

If we try to test the app, it would fail. The reason is that we locked down the app to accept request only from our instance of API Management:

Access Control

(A /32 means a single IP)

We can do that because the outbound public IP of an API Management Service is static (it doesn’t change). This is explained in the FAQ.

APIs

Let’s look at the APIs in API Management:

APIs

The Echo API comes as a sample with each API Management service and can be safely deleted.

The one-api is the one we deployed. It has three operations.

public

The public operation maps to the public-service Logic App.

It is configured as an HTTP-get taking two query string parameters. The transformation occurs in the inbound policies:

<inbound>
    <base />
    <set-backend-service base-url="{{public-service-url}}" />
    <set-body>@{
        var body=new
        {
            intro=context.Request.OriginalUrl.Query["intro"].First(),
            number=int.Parse(context.Request.Url.Query["number"].First())
        };

        return JObject.FromObject(body).ToString();
    }</set-body>
    <rewrite-uri template="{{public-service-query-string}}" copy-unmatched-params="false" />
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
</inbound>

A couple of things to say about those policies:

  • We store the backend-url in a named value, hence the double curly-braces. This is a good practice for two reasons:
    • It allows us to simplify the ARM template and alternating parameters between environments
    • It allows us to modify the policies in the portal, then export the ARM template and copy-paste the policy since it doesn’t contain any hard-coded values
  • The set-body policy is where we pick the HTTP-get query string parameter and serialized them in a JSON body
  • We add the Logic App query string containing its SAS token in a rewrite-uri policy ; again putting the query string as a named value facilitate the eventual rotation of secrets
  • We override the HTTP method to POST with a set-method policy
  • We forces the content-type to be of type application/json ; this is required for Logic Apps to interpret the body correctly

With only 19 lines of code we were able to transform the API front the frontend to the backend.

We can test the operation and get the simple result.

private-raw

This operation simply returns the response from the container. Nothing fancy here:

<inbound>
    <base />
    <set-backend-service base-url="{{private-service-url}}" />
    <rewrite-uri template="/" copy-unmatched-params="false" />
</inbound>

If we test the operation and get a result similar to:

<h3>Hello World!</h3><b>Hostname:</b> wk-caas-5fcd43c10fa6404e87ca36b291c59013-4dda47657e18158b8ed3d7<br/><b>Visits:</b> undefined

The string starting with wk-caas-5f… is the container ID. This one will vary each time the ACI is deployed.

private-select

Here we do some screen scraping. We used the same back-end service, i.e. the Azure Container Instance, but we post-process its response:

<outbound>
    <base />
    <set-body>@{
    var raw=context.Response.Body.As<string>(true);
    var startIndex = raw.IndexOf("</b>") + 4;
    var endIndex = raw.IndexOf("<br/>");
    var host = raw.Substring(startIndex, endIndex - startIndex).Trim();
    var response = new { hostName = host };

    return JObject.FromObject(response).ToString();
    }</set-body>
</outbound>

The C# code might look a little cryptic but basically, we do a couple of string manipulation to extract the container ID and return it in a JSON response. If we test it:

{
  "hostName": "wk-caas-5fcd43c10fa6404e87ca36b291c59013-4dda47657e18158b8ed3d7"
}

Here we can see how we can easily transform APIs again.

Products

Now if we look at the products, we see the standard Starter & Unlimited, but we also see two custom ones:

products

Subscription based product is pretty vanilla. The Token based one is interesting. If we look at its policies:

<inbound>
    <base />
    <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid." output-token-variable-name="jwt">
        <openid-config url="https://login.microsoftonline.com/{{tenant-name}}.onmicrosoft.com/.well-known/openid-configuration" />
        <audiences>
            <audience>{{app-id}}</audience>
        </audiences>
    </validate-jwt>
    <set-header name="Authorization" exists-action="delete" />
</inbound>

So here we do the JWT token validation at the product level. This means that every operation under every API belonging to that product will have this validation.

Shout out to Sacha Bruttin for his nice article which explains why we need the last line! The incoming requests will have an Authorization header which gets validated for JWT token. But if we don’t delete that header, it will be passed to Logic Apps (the backend API) which will fail with it. Hence the set-header policy deleting that header at the end.

Now the one-api API belongs to the two products: one requiring a JWT, one not requiring it. Products & API have a many-to-many relationship and here we exploit that for demo purposes.

If we go back to test the public operation but we explicitly choose the product as Token based:

Testing API without token

we will hit a failure:

HTTP/1.1 401 Unauthorized

content-length: 85
content-type: application/json
date: Fri, 12 Jul 2019 22:08:44 GMT
ocp-apim-trace-location: https://apimgmtstcnmes4lawzbtjoj.blob.core.windows.net/apiinspectorcontainer/pt8ZzbIWYyFjQG3dBlRRgg2-2?sv=2018-03-28&sr=b&sig=%2FnJUrpsZ85J3a9RvZmhAGAQ0mCQ43ZfV6ek46xGjCIU%3D&se=2019-07-13T22%3A08%3A45Z&sp=r&traceId=af662f0a7cd44949b15e61fe4f828000
vary: Origin
{
    "statusCode": 401,
    "message": "Unauthorized. Access token is missing or invalid."
}

That’s because the testing UI doesn’t send JWT token along with the request.

We can test that API with JWT token by using the public-consumer-token Logic App which does send a JWT token by using its Managed Service Identity (MSI).

Summary

We did cover a lot of ground here.

The main points to get across were:

  • Having public & private backends
  • Locking down public backends so we can’t bypass the API Management
  • Using OAuth / JWT as an authentication method
Advertisements

One thought on “API Management – OAuth and private back-ends

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s