Skip to content

on behalf of

Peter M edited this page Mar 16, 2023 · 35 revisions

If you are using ASP.NET Core.

If you are building a web API on to of ASP.NET Core, we recommend that you use Microsoft.Identity.Web. See Web APIs with Microsoft.Identity.Web.

You might want to check the decision tree: Is MSAL.NET right for me?.

Getting tokens on behalf of a user

Scenario

  • A client (web, desktop, mobile, single-page application) - not represented on the picture below - calls a protected web API, providing a JWT bearer token in its "Authorization" HTTP header.
  • The protected web API validates the incoming user token, and uses MSAL.NET AcquireTokenOnBehalfOf method to request from Azure AD another token so that it can, itself, call another web API (named the downstream web API) on behalf of the user.

This flow, named the On-Behalf-Of flow (OBO), is illustrated by the top part of the picture below. The bottom part is a daemon scenario, also possible for web APIs.

image

How to call OBO

The OBO call is done by calling the AcquireTokenOnBehalf method on the IConfidentialClientApplication interface.

This call looks in the cache by itself - so you do not need to call AcquireTokenSilent and does not store refresh tokens.

For scenarios where continuous access is needed without an assertion, see OBO for long lived processes

Note: Make sure to pass an access token, not an ID token, into the AcquireTokenOnBehalfOf method. The purpose of an ID token is as a confirmation that a user was authenticated and it contains some user-related information. While an access token determines whether a user has access to a resource, which is more appropriate in this On-Behalf-Of scenario. MSAL is focused on getting good access tokens. ID tokens are also obtained and cached but their expiry is not tracked. So it's possible for an ID token to expire and AcquireTokenSilent will not refresh it.

private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityToken jwtToken, ClaimsPrincipal principal, HttpContext httpContext)
{
  UserAssertion userAssertion;
  IEnumerable<string> requestedScopes;
  if (jwtToken != null)
  {
   userAssertion = new UserAssertion(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
   requestedScopes = scopes ?? jwtToken.Audiences.Select(a => $"{a}/.default");
  }
  else
  {
   throw new ArgumentOutOfRangeException("tokenValidationContext.SecurityToken should be a JWT Token");
  }

  // Create the application
  var application = BuildConfidentialClientApplication(httpContext, principal);

  // .Result to make sure that the cache is filled-in before the controller tries to get access tokens
  var result = await application.AcquireTokenOnBehalfOf(
           requestedScopes.Except(scopesRequestedByMsalNet),
           userAssertion)
              .ExecuteAsync()                     
}

Long-running OBO processes

One OBO scenario is when a web API runs long running processes on behalf of the user (for example, OneDrive which creates albums for you). This can be implemented as such:

  1. Before you start a long running process, call:
string sessionKey = // custom key or null
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
         .InitiateLongRunningProcessInWebApi(
              scopes,
              userAccessToken,
              ref sessionKey)
         .ExecuteAsync();

userAccessToken is a user access token used to call this web API. sessionKey will be used as a key when caching and retrieving the OBO token. If set to null, MSAL will set it to the assertion hash of the passed-in user token. It can also be set by the developer to something that identifies a specific user session, like the optional sid claim from the user token (for more information, see Provide optional claims to your app). InitiateLongRunningProcessInWebApi doesn't check the cache; it will use the user token to acquire a new OBO token from AAD, which will then be cached and returned.

  1. In the long-running process, whenever OBO token is needed, call:
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
         .AcquireTokenInLongRunningProcess(
              scopes,
              sessionKey)
         .ExecuteAsync();

Pass the sessionKey which is associated with the current user's session and will be used to retrieve the related OBO token. If the token is expired, MSAL will use the cached refresh token to acquire a new OBO access token from AAD and cache it. If no token is found with this sessionKey, MSAL will throw a MsalClientException. Make sure to call InitiateLongRunningProcessInWebApi first.

Cache eviction for long-running OBO processes

It is strongly recommended to use a distributed persisted cache in a web API scenario. Since these APIs store the refresh token, MSAL will not suggest an expiration, as refresh tokens have a long lifetime and can be used over and over again.

It is recommended that you set L1 and L2 eviction policies manually, for example a max size for the L1 cache and a sliding expiration for the L2.

Exception handling

In a case when AcquireTokenInLongRunningProcess throws an exception when it cannot find a token and the L2 cache has a cache entry for the same cache key, verify that the L2 cache read operation completed successfully. AcquireTokenInLongRunningProcess is different from the InitiateLongRunningProcessInWebApi and AcquireTokenOnBehalfOf, in that it is if the cache read fails, this method is unable to acquire a new token from AAD because it does not have an original user assertion. If using Microsoft.Identity.Web.TokenCache to enable distributed cache, set OnL2CacheFailure event to retry the L2 call and/or add extra logs, which can be enabled like this.

Removing accounts

Starting with MSAL 4.51.0, to remove cached tokens call StopLongRunningProcessInWebApiAsync passing in a cache key. With earlier MSAL versions, it is recommended to use L2 cache eviction policies. If immediate removal is needed, delete the L2 cache node associated with the sessionKey.

App registration - specificities for Web APIs

Practical usage of OBO in an ASP.NET / ASP.NET Core application

If you are building a web API on to of ASP.NET Core, we recommend that you use Microsoft.Identity.Web. See Web APIs with Microsoft.Identity.Web.

In an ASP.NET / ASP.NET Core Web API, OBO is typically called on the OnTokenValidated event of the JwtBearerOptions. The token is then not used immediately, but this call has the effect of populating the user token cache. Later, the controllers will call AcquireTokenSilent, which will have the effect of hitting the cache, refreshing the access token if needed, or getting a new one for a new resource, but for still for the same user.

Here is what happens when a Jwt bearer token is received end validated by the Web API:

public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
{
 ...
 services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
 {
  options.Events.OnTokenValidated = async context =>
  {
   var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
   context.Success();

   // Adds the token to the cache, and also handles the incremental consent and claim challenges
   tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
   await Task.FromResult(0);
  };
 });
 return services;
}

And here is the code in the actions of the API controllers, calling downstream APIs:

private async Task GetTodoList(bool isAppStarting)
{
 ...
 //
 // Get an access token to call the To Do service.
 //
 AuthenticationResult result = null;
 try
 {
  result = await _app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault())
                     .ExecuteAsync()
                     .ConfigureAwait(false);
 }
...

// Once the token has been returned by MSAL, add it to the http authorization header, before making the call to access the To Do list service.
// Make sure to use an access token and not an ID token
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

// Call the To Do list service.
HttpResponseMessage response = await _httpClient.GetAsync(TodoListBaseAddress + "/api/todolist");
...
}

the GetAccountIdentifier method uses the claims associated with the identity of the user for which the Web API received the JWT:

public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal)
{
 string userObjectId = GetObjectId(claimsPrincipal);
 string tenantId = GetTenantId(claimsPrincipal);

 if (!string.IsNullOrWhiteSpace(userObjectId) && !string.IsNullOrWhiteSpace(tenantId))
 {
  return $"{userObjectId}.{tenantId}";
 }

 return null;
}

Protocol

For more information about the On-Behalf-Of protocol, see Azure Active Directory v2.0 and OAuth 2.0 On-Behalf-Of flow.

Samples illustrating the on-behalf of flow

Sample Platform Description
active-directory-aspnetcore-webapi-tutorial-v2 ASP.NET Core 2.2 Web API, Desktop (WPF) ASP.NET Core 2.1 Web API calling Microsoft Graph, itself called from a WPF application using Azure AD V2 topology

Vanity URL: https://aka.ms/msal-net-on-behalf-of

Getting started with MSAL.NET

Acquiring tokens

Web Apps / Web APIs / daemon apps

Desktop/Mobile apps

Advanced topics

FAQ

Other resources

Clone this wiki locally