Service Principal Configuration
Azure APIM Configuration and Policy
Architecture
- The end-user makes an autheticated request to the middle tier/OBO application. Token audience: OBO application
- The OBO application validates the access token from the user
- The OBO application requests a new access token from the backend application on behalf of the user
- 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:
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:
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)
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 **
- Token generated on behalf of the user when the OBO app authenticates with the bakcned app **
Limitations
- 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.