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

Azure API Management – JWT validation for multiple Azure AD partner registrations

If there is one popular theme regarding API management, it’s gotta be the subject of facilitating technically partner access via Azure AD. This can be as simple as creating a single app registration to designing full governance model with scopes & roles, and chained App Registrations.

As always, if you want to generally know what is Azure API management, or what is Azure Active Directory, then check the following articles:

Architecture

  • The architecture with new app endpoints enables same app registration to provide native and confidential client access with different settings
  • Scopes are defined per each partner registration

Create Centralized API Management Proxy SPN

  • Here we want to create single-tenant app in order to be able to control roles and authorizations within our tenant for B2B accounts. In multi-tenant scenario this is possible too, but requires that you control most of the authorizations in another management plane, or in scopes only
  • For redirect URI we input nothing, because this application will only act as Service Principal ”Front” for the actual app registrations consuming API management
  • We will use the FQDN of API management as displayName and AppID URL, in later calls we will use this as ’Audience /Resource for the partner clients”

Create Scopes and machine account role for the SPN

There is GUI to define scopes, but I have mine ready on template, so I am gonna use the ”old” experience updating via the manifest to create the scopes
oAuth2Permissions updated via the ”old experience” – Update ”Oauth2Permissions and AppRoles blocks”
 $apps = '{
  "oauth2Permissions": [
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda",
         "id":  "bc388acd-37a3-4358-8839-c84b4add4b23",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "user_impersonation"
     },
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda",
         "id":  "40cfe68d-bef2-4a4b-8944-fc9766ba17d8",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "user_impersonation2"
     },
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user as reader.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda -reader",
         "id":  "a1203e4c-54bf-4e92-bb61-73e1e347cf9d",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf as writer to the API.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda -reader",
         "value":  "Reader"
     },
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user as Writer.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda -writer",
         "id":  "b68e3e4f-7f45-497f-a815-6dc6ef46bbc8",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf as Writer.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "Writer"
     },
     {
         "adminConsentDescription":  "Service Account user Scope",
         "adminConsentDisplayName":  "Service Account App permissions",
         "id":  "508b851a-7393-478a-89b0-f83d1dcc825b",
         "isEnabled":  true,
         "type":  "Admin",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf as Writer.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "Service_Accounts"
     }
                          ]
  }' | ConvertFrom-Json

  foreach ( $app in ($apps.oauth2Permissions)) {
  $app.id =([system.guid]::NewGuid()).guid
  }

  $apps |ConvertTo-Json -Depth 3

#Roles


$apps ='{
"appRoles":  [
      {
          "allowedMemberTypes":  [
                                     "application"
                                 ],
          "displayName":  "ReadOnly-ServiceAccount",
          "id":  "03b62322-1913-4c61-86a7-c325ebd5925f",
          "isEnabled":  true,
          "description":  "ReadOnly-ServiceAccount only to read data.",
          "value":  "ReadOnly-ServiceAccount"
      },
      {
          "allowedMemberTypes":  [
                                     "application"
                                 ],
          "displayName":  "ServiceAccounts",
          "id":  "a59b7301-039e-4782-ad1f-fc443cce1786",
          "isEnabled":  true,
          "description":  "Machine 2 Machine -type integrations.",
          "value":  "ServiceAccount"
      }
  ]
    
}' | ConvertFrom-Json 

  foreach ( $app in ($apps.appRoles)) {
  $app.id =([system.guid]::NewGuid()).guid
  }

  $apps |ConvertTo-Json -Depth 3

Create first partner application

  1. The first partner application is for typical use scenario of Client-Credentials Flow and Authorization Code Flow
From API permissions which we just defined on the manifest we can choose, what kind of user permissions (delegated) and application permissions we can set
In this case the partner needs full serviceAccount permissions, which I’ve set require admin consent
Since there is admin permission required I grant the directory access for the App. In the demo this will grant full scopes for the user. Later we can adjust the behavior of consents and pre-consents quite a lot if needed
Optionally, instead of ”allPrincipals consent” you have the option to leave the consent to the end user, if you’ve decided to make some of the scope end user consent -able (Example is the second partner API2)

Configure JWT validation for Partner Application

Each Partner is identified with their unique App ID claim. The control Any is used, to enable proxying of multiple Partner Clients. 401 is thrown if valid and signature checked AppID isnt found in the incoming Bearer Token
  • Note how we don’t need to reference the Proxy SPN at all? That’s because in previous step we’ve configured permissions for the Partner API to consume the Proxy SPN of API Management, and we need to reference the correct audience only

Test the API as a Partner using Code Authorization Flow

  • Response is what you would expect for Echo API’s create with POST-Verb
We are using ADAL binaries to invoke the code authorization flow
  • For PartnerClientID, we use the AppID we created in the previous step
  • We are using the public client redirect-uri to return the token for the user
This is how the Token Maps to JWT validation in Echo API

Test the API as a Partner using Client Credentials Flow

btw… don’t boughter with the client secret 🙂
  • Response is what you would expect for Echo API’s create with POST-Verb

Thats it for now! I will provide more insights, how to use multi-tenant apps, and how transform scopes into correlating back-end calls in the next blog