Concept: Publish on-prem API using AAD App Proxy and API Management with Azure AD JWT Bearer Grant

Disclaimer: Azure AD App Proxy is perfectly capable of covering most of the internal API publishing scenarios, If you can handle API request and response handling with just client and on-premises server. Alternatively you might have another component on-prem which can act as middle-tier component to do further validation and shaping of requests. In a nutshell, if you’re API-scenario doesn’t benefit from from middle-tier service, then I suggest you continue with ”keeping-it-simple” /And as always, all Disclaimer: The information in this weblog is provided “AS IS” with no warranties and confers no rights.

Better together?

API Management and AAD App Proxy can complement each other when you need to have request shaping / central API Gateway for processing before calling back-end API’s – In this blog I explore a PoC example, and some reasoning for such scenarios

When to use? / benefits

  • You’re internal API isn’t visible to Azure API management via on-premises network connectivity, and you’re not planning to use site-site networking in the future, or for a particular API
  • You want to enrich payloads and headers of requests for particular back-end services. For example services which cant consume claims in JWT Tokens. You also want to ensure, that selected parts of these payloads cannot be forged by the client
  • Have single end-point to distribute and shape/manipulate traffic to various API’s (General argument)
Extra Claims from APIM

How it works (short)

  • APIM calls the App Proxy SPN instead of mobile clients
  • The ServicePrincipals for APIM and AppProxy App are blocked for providing access tokens via Authorization Code Grant (removal of redirect uri). Leaving only option that APIM will use the JWT-Bearer grant. This ensures, that only the APIM can fetch the ”final” access token, for the App Proxy App
  • Flow: Native client (Auth Code Flow) -> APIM (JWT-Bearer Grant) -> Azure AD App Proxy SPN Authorization (Permissions to make this work are explained on later part of this blog)

Ensuring integrity with retrofit of AAD App Proxy & APIM

  • In order to retrofit the Azure AD Application Proxy with APIM it’s essential that the App Proxy Application and APIM SPN can act only as as Web API’s (not public clients) this keeps the flow intact
    • Stripping token issuance rights of the SPN for Authorization Code flow, ensures: That only the App registration for APIM can delegate user access to the App Proxy SPN (Audience) using that particular flow
    • This assumption only works when you don’t retroactively enable Implicit Flow on the SPN itself. (You can have Implicit grant on other clients, but not on the this particular SPN, which is the owner of the AppProxy Audience ( identifierUri)

How To?

  • Remove redirect URI’s from both middle-stream API and App Proxy Application
  • Then perform the fencing (below) by delegating rights in correct order to support the flow
  • Now the public client, can only get tokens for APIM, but can never call App Proxy directly, as the client doesn’t have direct permissions on the App Proxy SPN (Only the APIM has)

Azure AD Fencing: Utilizing extended grant types to access downstream API’s

Click the picture for bigger version
OAuth2.0 On-Behalf-Of flow
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#protocol-diagram

Using Azure AD Specific GrantType (urn:ietf:params:oauth:grant-type:jwt-bearer) is the magic component we use here.

  • JWT bearer flow allows us to create ”DMZ-like” fencing between direct calls, and downstream calls destined AppProxy SPN with Middle-tier API

Using the On-Behalf-Of flow (JWT bearer), we can ensure that APIM is the only allowed caller for the App Proxy Audience

Click the picture for bigger version

API Management configuration

  • The following policy is ”tip of the ice berg” in terms of how you can shape, and handle requests bound to multiple directions
  • There is possibility of doing more graceful handling with more the multiple policy clauses APIM provides
  • I can hardly claim any credit (apart architectural and flow design) for the APIM policies below, as web is full great APIM examples for all of the policies I have used below
 
<policies>
    <inbound>
        <!-- validate the initial call destined later towards middle-tier API-->
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://login.microsoftonline.com/dewired.onmicrosoft.com/.well-known/openid-configuration" />
            <audiences>
                <audience>https://webapi-a.dewi.red</audience>
            </audiences>
            <required-claims>
                <claim name="appid" match="any">
                    <value>299568d2-3036-41d9-a961-89266e67ea82</value>
                </claim>
            </required-claims>
        </validate-jwt>
        <!-- Forward THE UPN header to back-end -->
        <set-variable name="UPN" value="@(context.Request.Headers["Authorization"].First().Split(' ')[1].AsJwt()?.Claims["upn"].FirstOrDefault())" />
        <set-variable name="Bearer" value="@(context.Request.Headers["Authorization"].First().Split(' ')[1])" />
        <set-header name="back-endUPN" exists-action="override">
            <value>@(context.Variables.GetValueOrDefault<string>("UPN"))</value>
        </set-header>
        <!-- Send new request with the Token -->
        <send-request mode="new" response-variable-name="OBOtoken" timeout="20" ignore-error="false">
            <set-url>{{tokenURL2}}</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="User-Agent" exists-action="override">
                <value>Mozilla/5.0 (Windows NT; Windows NT 10.0; fi-FI) WindowsPowerShell/5.1.17763.503</value>
            </set-header>
            <set-body>@{
            var tokens = context.Variables.GetValueOrDefault<string>("Bearer");
           
              return "assertion=" + tokens + @"&client_id={{clientid2}}&resource={{resource}}&client_secret={{ClientSecret}}&grant_type={{grantType}}&requested_token_use={{requested_token_use}}";
             
               }</set-body>
        </send-request>
        <!-- Forward the OBOtoken to AppProxy  -->
        <choose>
            <when condition="@(((IResponse)context.Variables["OBOtoken"]).StatusCode == 200)">
                <set-variable name="OBOBearer" value="@(((IResponse)context.Variables["OBOtoken"]).Body.As<JObject>(preserveContent: true).GetValue("access_token").ToString())" />
                <set-variable name="Debug" value="@(((IResponse)context.Variables["OBOtoken"]).Body.As<JObject>(preserveContent: true).ToString())" />
                <set-header name="Authorization" exists-action="override">
                    <value>@{
                    var ForwardToken = context.Variables.GetValueOrDefault<string>("OBOBearer");
                    return "Bearer "+ ForwardToken;
                    }</value>
                </set-header>
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
            </when>
        </choose>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies> 

Testing the solution in action

Before testing ensure that all places where you’ve defined audiences are explicitly matching for the audience (App registrations, Named Values, APIM policies, Clients requesting the access)

There are multiple ways to test the solution, but testing through APIM’s test console, and peeking the in-secure back-end resource via HTTP trace yields the most verbose results

  • Get Access Token for the API-A (APIM) with ”bulk client”
  • Paste the access token to APIM test console and perform test call to view traces
  • Check the back-end for results
Click for bigger picture

And that’s it!

Further stuff

  • Use keyVault instead of named values (secret) for storing secrets
named values
  • Place WAF to the front of APIM
  • fine tune to the policies in APIM ( this was just the PoC)
    • For example, the back-end could use cached token for the downstream call, as the user, and user-identity is validated in the first step (its validated in the second step also)
https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-cache
  • Get some proficiency in C# syntax … As PS and JavaScript fellow, I found myself seriously struggling to properly escape, cast and enumerate variables/content

P.S. My similar articles