Azure AD OBO Flow With App Proxy And Azure API Manager

Posted on
azureAD APIM API Service Principal App Proxy Auth Code Grant Type

Architecture

Backend App: App Proxy

Service Principal Configuration

Azure APIM Configuration and Policy

Comparing Access Tokens

OBO Flow Limitations

Conclusion

Architecture

logical architecture and flow diagram

  1. The end-user makes an autheticated request to the middle tier/OBO application. Token audience: OBO application
  2. The OBO application validates the access token from the user
  3. The OBO application requests a new access token from the backend application on behalf of the user
  4. the OBO application forwards the request on-behalf of the user with the new token that has its (the OBO app) client ID as audience

Backend App

In my use-case, I configured an internal backend app using Azure AD App Proxy

App Proxy creates a Service Principal in AzureAD that allows users from the tenant to authenticate. In my configuration, the OBO app would be the only one permitted API permissions:

backend app configuration

In addition to this, to avoid issues with Admin consent, Add a scope to the backend app, then assign the OBO app app by authorizing its Client ID:

backend app api permissions

SP Configuration

For the backend app, the following configuration are needed:

  • redirect URL (mine is http://localhost:3000)
  • get a client secret: save this to usse later
  • API Permissions: Microsoft Graph: User.Read (Delegated)
  • Expose API: Add a scope that will be user in backend app API permissions configuration. This will give you a scope that looke like this: api:<client_id>/Scope.Name <– Note this, we’ll use it in our calls later.

APIM Configuration

In this example, we’re using APIM as the middle tier app that uses the OBO/Middle tier application registration details to authenticate our end users against AzureAD (hence the User.Read MS Graph permissions in SP Configuration step above).

APIM Named Valies (aka variables to be used in Policies)

More details about why we need these can be found here: Azure Docs: OAuth 2.0 On-Behalf-Of flow

  • assertion
  • client_id
  • client_secret
  • grant_type
  • request_token_use
  • scope

Named Value Tip: The display name is what you add in APIM as variable (more in APIM Policy Configuration)

backend app api permissions

APIM Policy Configuration

A lot of leg work was done in this blog on the policy front. I just made a few additions

<policies>
    <inbound>
        <base />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="APIM Authentication failure">
            <openid-config url="https://login.microsoftonline.com/<tenant_id>/.well-known/openid-configuration" />
            <required-claims>
                <claim name="aud" match="all">
                    <value><apim_middle_tier_app_client_ID></value>
                </claim>
            </required-claims>
        </validate-jwt>
        <set-variable name="Bearer" value="@(context.Request.Headers["Authorization"].First().Split(' ')[1])" />
        <send-request mode="new" response-variable-name="OBOtoken" timeout="20" ignore-error="false">
            <set-url>https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token</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={{client_id}}&client_secret={{client_secret}}&grant_type={{grant_type}}&requested_token_use={{requested_token_use}}&scope={{scope}}";
            //return "assertion={{assertion}}&client_id={{client_id}}&client_secret={{client_secret}}&grant_type={{grant_type}}&requested_token_use={{requested_token_use}}&scope={{scope}}";
               }</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>
                <!-- Adding new headers to the fowarded request -->
                <set-header name="Rolands-Header" exists-action="override">
                    <value>From-APIM</value>
                </set-header>
                <!-- adding this header to ensure the request am seeing is indeed from APIM -->
                <set-header name="x-apim-backend-host" exists-action="override">
                    <value>@(context.Request.Url.Host)</value>
                </set-header>
                <!-- this is the new token that the OBO app received on behalf of the user -->
                <set-header name="x-OBOBearer" exists-action="override">
                    <value>@{
                    var ForwardToken = context.Variables.GetValueOrDefault<string>("OBOBearer");
                    return "Bearer "+ ForwardToken;
                    }</value>
                </set-header>
            </when>
        </choose>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Compare Access Tokens

User request configuration (Can only be Auth Code Grant Type see more) You can download the Postman Collection from here. Don’t forget to modify the Authorization values and request URL

Once configured, you should be able to make a request to the middle tier

  • OBO Token generated by the Identity platform when the user authenticates with the OBO app ** backend app api permissions
  • Token generated on behalf of the user when the OBO app authenticates with the bakcned app ** backend app api permissions

Limitations

See more here: Azure Docs

  • The OBO flow only works for user principals at this time. A service principal cannot request an app-only token, send it to an API, and have that API exchange that for another token that represents that original service principal.
  • The OBO flow is focused on acting on another party’s behalf, (i.e. delegated scenario), which means that it uses only delegated scopes, and not application roles, for reasoning about permissions. Roles remain attached to the principal (the user) in the flow, never the application operating on the users behalf.
  • Note that some implicit-flow derived id_token can’t be used for OBO flow

Conclusion

Am sure there are many use cases for this flow, but I’d recommend to closely question the reason why, especially given the limitations mentioned above.

In most cases, a normal flow is enough. With conditional access policies like MFA, sign-in risk policies, trusted locations, etc, there’s anough security in place to grant users direct access to the app in most use cases.