Wednesday, November 1, 2023

Azure API Management: Enriching Requests with Additional Headers and Use of Caching

In this post let's see how we can customize Azure API Management (APIM) Policy to enrich requests with additional headers and also let's look at how we can make use of caching.

The demo scenario is as follows:
  • I have an example BE API, which requires a specific HTTP Header: X-Tenant-Code, in order for it to serve.
  • A client sends a request with JWT Token, but the client has no knowledge of X-Tenant-Code HTTP header.
  • Inside the APIM, we need to inspect the JWT Token, and based on a Claim in the token (in this case, it's AppId/ClientId), we need to find out Tenant information by calling another API endpoint, let's say Tenant API. And then populate X-Tenant-Code HTTP header from the response data.
  • We don't need to be calling the Tenants API all the time, instead, we can cache Tenant information in APIM itself (while we can use the inbuilt cache, it's recommended to use external Cache, currently only Redis is supported)
Now let's start.

1: The first step is accessing the JWT token and extracting claims. (Read more on how to access JWT token: API Management policy expressions)
<!-- Get ClientId From JWT Token -->
<set-variable  name="clientid" 
    value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Claims.GetValueOrDefault("appid"))" />
2: Now I am checking whether the Tenant Information already exists in APIM Cache for the given ClientId.
<!--Look for tenantinfo for this specific client in the cache and set it to context variable 'tenantinfo' -->
<cache-lookup-value  key="@("tenantinfo-" + context.Variables["clientid"])"  variable-name="tenantinfo" />
3: Now I need to have a conditional expression. That is, if the item is not in the cache, then I need to call another API endpoint, passing in the ClientId to get the Tenant information. Let's assume, the endpoints return a  JSON response.
<choose>
<!-- Didn't find tenantinfo in cache -->
    <when condition="@(!context.Variables.ContainsKey("tenantinfo"))">
     <!-- Make a HTTP request to GET Tenant, copying current HTTP request headers -->
        <!-- Store the response in context variable 'tenantinforesponse' -->
        <send-request mode="copy" response-variable-name="tenantinforesponse" timeout="10" ignore-error="true">
         <set-url>@("https://app-azarch-tenants-service-001.azurewebsites.net/" + context.Variables["clientid"])</set-url>
            <set-method>GET</set-method>
        </send-request>

        <!-- Store response body in context variable 'tenantinfo' as a JObject -->
        <set-variable  name="tenantinfo"  value="@(((IResponse)context.Variables["tenantinforesponse"]).Body.As<JObject>())" />

        <!-- Store result in cache for 1 day (86400 seconds) -->
        <cache-store-value  key="@("tenantinfo-" + context.Variables["clientid"])"  value="@((JObject)context.Variables["tenantinfo"])"  duration="86400" />
    </when>
</choose>
4: At this state, the context variable: tenantinfo , is populated. And now we can use it to populate X-Tenant-Code header.
<!-- Set the X-Tenant-Code header to the tenantCode value from the context variable 'tenantinfo' -->
<set-header name="X-Tenant-Code" exists-action="override">
<value>@((string)((JObject)context.Variables["tenantinfo"]).GetValue("tenantCode"))</value>
</set-header>
The complete policy looks as follows.
<policies>
    <inbound>
        <!-- Get ClientId From JWT Token -->
        <set-variable name="clientid" 
            value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Claims.GetValueOrDefault("appid"))" />

        <!--Look for tenantinfo for this specific client in the cache and set it to context variable 'tenantinfo' -->
        <cache-lookup-value key="@("tenantinfo-" + context.Variables["clientid"])" variable-name="tenantinfo" />

        <choose>
            <!-- Didn't find tenantinfo in cache -->
            <when condition="@(!context.Variables.ContainsKey("tenantinfo"))">
              
                <!-- Make a HTTP request to GET Tenant, copying current HTTP request headers -->
                <!-- Store the response in context variable 'tenantinforesponse' -->
                <send-request mode="copy" response-variable-name="tenantinforesponse" timeout="10" ignore-error="true">
                    <set-url>@("https://app-azarch-tenants-service-001.azurewebsites.net/" + context.Variables["clientid"])</set-url>
                    <set-method>GET</set-method>
                </send-request>

                <!-- Store response body in context variable 'tenantinfo' as a JObject -->
                <set-variable 
                    name="tenantinfo" 
                    value="@(((IResponse)context.Variables["tenantinforesponse"]).Body.As<JObject>())" />

                <!-- Store result in cache for 1 day (86400 seconds) -->
                <cache-store-value 
                     key="@("tenantinfo-" + context.Variables["clientid"])" 
                     value="@((JObject)context.Variables["tenantinfo"])" 
                     duration="86400" />
                      
            </when>
        </choose>

        <!-- Set the X-Tenant-Code header to the tenantCode value from the context variable 'tenantinfo' -->
        <set-header name="X-Tenant-Code" exists-action="override">
            <value>@((string)((JObject)context.Variables["tenantinfo"]).GetValue("tenantCode"))</value>
        </set-header>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
And that's it.

You can set up External Cache, from the following menu item.
APIM: External Cache
Using an external cache can be really handy if you want to inspect the cache items more visually. Something like below (I wrote a post a while ago on Connecting to Azure Cache for Redis Instance from RedisInsight, do check it out).
APIM Cache
Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment