This article is contributed. See the original author and article here.

Co-authors (alphabetical order):


Dan Balma, Maarten Van De Bospoort, Vishnu Naga Praveen Deepthimahanthi, Nick Drouin, Kreig DuBose, David Giard, Michael Green, Binay Kumar, Hao Luo, Shubhaangi Mahajan, Andres Robinet, Jatin Sharma, Taru Sinha, David Triana, Jeremy Woo-Sam, Franco Zuccarelli


 


Introduction


API Management can acquire access tokens from backend before forwarding calls with the access token to the backend. This document shows how to acquire access token from Azure AD thru client credentials flow. Here we present an API Management policy which can not only acquire access token, but also cache and renew upon its expiration.


In addition, we assume the backend service is not necessarily protected by Azure AD. If backend is one or multiple different vendors’ services protected by different Identity Providers and token issuers, we can use API Management as a gateway to achieve the following goals:



  1. Replacing multiple different backend identity providers/token issuers by a single one: Azure AD, to protect the list of backend REST API services. An Azure application can use any of the OAuth2 grant flows with a single Azure-native Identity Provider: Azure AD and its token issuer to access the backend services.

  2. Shielding an Azure application and its security from backend (vendor specific) security schemes. In case any of the backend (vendor) systems is replaced, what needs to be changed is limited to API Management policy, instead of Azure application code. The same Azure AD tenant, users, groups, managed identities, service principals, roles and RBAC can stay intact.


 These goals are described by the following diagram.


 


security_arch.drawio.png


 


Assumptions


Since OAuth2 and JSON Web Token (JWT) are today’s default choices in implementing authorization, this API Management policy is built on the following assumptions:



  1. Access token is of JWT format;

  2. In this API Management policy, we assume the backend uses ROPC (Resource Owner Password Credentials) grant flow. If the backend uses another flow (such as client credentials), corresponding code change is needed but the code change is limited to token acquisition. The code for token caching and expiration can stay intact. This document provides a sample policy for acquiring access token from Azure AD using client credentials flow.


Design Decisions



  1. We have chosen to use API Management internal cache for caching token. But switching to external cache requires only minor change. Except for Consumption tier, all other tiers of API Management support internal cache. See here for details.

  2. We have chosen to cache both access token and its expiration time. With this decision, during cache hit (most of the time), there is no need to parse the JWT for its expiration (exp claim value). The exp claim value is parsed only once for each token upon token acquisition from token endpoint.

  3. We have chosen to set maximum token cache duration to 60 minutes (see details below). This can be changed easily.


The API Management Policy


The API Management policy is shown below. The basic flow:



  • In case of cache miss or cache hit but token has expired, an access token is acquired (in this case, via Resource Owner Password Credentials flow). Then the expiration time is parsed. Both the access token and its expiration are added into cache.

  • In case of cache hit and the cached token has not expired, the cached token is used.

  • In either case, the access token is set in Authorization header as a bearer token before forwarding the call to the backend specified by {{svc_base_url}}.

  • The API Management subscription key header is removed in case it is present.


 


 

<policies>
    <inbound>
        <base />
        <set-backend-service base-url="{{svc_base_url}}" />
        <cache-lookup-value key="{{svc_base_url}}-token-key" variable-name="token" caching-type="internal" />
        <cache-lookup-value key="{{svc_base_url}}-token-exp-key" variable-name="token-exp" caching-type="internal" />
        <choose>
            <when condition="@(!context.Variables.ContainsKey("token") || 
                               !context.Variables.ContainsKey("token-exp") ||
                               (context.Variables.ContainsKey("token") && 
                                context.Variables.ContainsKey("token-exp") && 
                                (DateTime.Parse((String)context.Variables["token-exp"]).AddMinutes(-1.0) 
                                 <= DateTime.UtcNow) 
                               )
                            )">
                <send-request ignore-error="false" timeout="{{svc_token_acquisition_timeout}}" response-variable-name="jwt" mode="new">
                    <set-url>{{svc_token_endpoint}}</set-url>
                    <set-method>POST</set-method>
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/x-www-form-urlencoded</value>
                    </set-header>
                    <set-header name="Authorization" exists-action="override">
                        <value>@("Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("{{svc_client_id}}:{{svc_client_secret}}")))</value>
                    </set-header>
                    <set-body>@("username={{svc_username}}&password={{svc_password}}&grant_type=password")</set-body>
                </send-request>
                <set-variable name="token" value="@((String)((IResponse)context.Variables["jwt"]).Body.As<JObject>()["access_token"])" />
                <set-variable name="token-exp" value="@{
                    string jwt = (String)context.Variables["token"];
                    string base64 = jwt.Split('.')[1].Replace("-", "+").Replace("_", "/");
                    int mod4 = base64.Length % 4;
                    if (mod4 > 0)
                    {
                        base64 += new String('=', 4 - mod4);
                    }
                    string base64_encoded = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(base64));
                    double exp_num = (double)JObject.Parse(base64_encoded)["exp"];
                    DateTime exp = (new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).AddSeconds(exp_num);
                    return exp.ToString("MM-dd-yyyy HH:mm:ss");
                }" />
                <cache-store-value key="{{svc_base_url}}-token-key" value="@((String)context.Variables["token"])" duration="3600" caching-type="internal" />
                <cache-store-value key="{{svc_base_url}}-token-exp-key" value="@((String)context.Variables["token-exp"])" duration="3600" caching-type="internal" />
            </when>
        </choose>
        <set-header name="Authorization" exists-action="override">
            <value>@{
                return $"Bearer {(String)context.Variables["token"]}";
            }</value>
        </set-header>
        <set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

 


 


Features


The API Management policy has the following features:



  1. For each incoming REST call, API Management acquires access token from backend on its behalf and replaces or adds the Authorization header with the access token as a bearer token before forwarding the call to the backend service.

  2. All backend system security credentials are stored in an Azure Key Vault and API Management retrieves them for token acquisition thru API Management Named Value feature. Note: today Terraform does not support API Management Named Value directly linked to a Key Vault. Hence we should consider “moving” credential value (from Key Vault) into API Management Named Value (secret type) via Terraform during deployment. In any case, the policy stays the same regardless whether a credential is in Named Value as a secret or linked to Key Vault secret.

  3. Access token is cached, which could improve performance by 60% or more as observed;

  4. Every JWT access token expires. Upon token expiration, expired token will be replaced by a new one.

  5. Cache duration cap: some token issuers set very long token lifetime which is not a recommended security practice. We put a cap on token lifetime thru API Management policy, so that cached token never ages over, say one hour, like what Azure AD does, regardless the expiration settings of tokens.

  6. By design, API Management cache key is scoped to the whole API Management instance including all APIs deployed in the instance. We have made sure that token cache key is scoped to an API in an API Management instance, avoiding any possible cache key conflict among APIs deployed within an API Management instance.

Brought to you by Dr. Ware, Microsoft Office 365 Silver Partner, Charleston SC.