Web API that calls web APIs - code configuration

After you've registered your web API, you can configure the code for the application.

The code to configure your web API so that it calls downstream web APIs builds on top of the code used to protect a web API. For more info, see Protected web API - app configuration.

Code subscribed to OnTokenValidated

On top of the code configuration for any protected web APIs, you need to subscribe to the validation of the bearer token that's received when your API is called:

/// <summary>
/// Protects the web API with Microsoft Identity Platform (a.k.k AAD v2.0)
/// This supposes that the configuration files have a section named "AzureAD"
/// </summary>
/// <param name="services">Service collection to which to add authentication</param>
/// <param name="configuration">Configuration</param>
/// <returns></returns>
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services,
                                                             IConfiguration configuration,
                                                             IEnumerable<string> scopes)
{
    services.AddTokenAcquisition();
    services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
    {
        // When an access token for our own web API is validated, we add it 
        // to MSAL.NET's cache so that it can be used from the controllers.
        options.Events = new JwtBearerEvents();

        options.Events.OnTokenValidated = async context =>
        {
            context.Success();

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

On-behalf-of flow

The AddAccountToCacheFromJwt() method needs to:

  • Instantiate an MSAL confidential client application.
  • Call AcquireTokenOnBehalf to exchange the bearer token that was acquired by the client for the web API, against a bearer token for the same user, but for our API to call a downstream API.

Instantiate a confidential client application

This flow is only available in the confidential client flow so the protected web API provides client credentials (client secret or certificate) to the ConfidentialClientApplicationBuilder via the WithClientSecret or WithCertificate methods, respectively.

image

IConfidentialClientApplication app;

#if !VariationWithCertificateCredentials
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
           .WithClientSecret(config.ClientSecret)
           .Build();
#else
// Building the client credentials from a certificate
X509Certificate2 certificate = ReadCertificate(config.CertificateName);
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
    .WithCertificate(certificate)
    .Build();
#endif

Finally, instead of a client secret or a certificate, confidential client applications can also prove their identity using client assertions. This advanced scenario is detailed in Client assertions

How to call on-behalf-of

The on-behalf-of (OBO) call is done by calling the AcquireTokenOnBehalf method on the IConfidentialClientApplication interface.

The UserAssertion is built from the bearer token received by the web API from its own clients. There are two constructors, one that takes a JWT bearer token, and one that takes any kind of user assertion (another kind of security token, which type is then specified in an additional parameter named assertionType).

image

In practice, the OBO flow is often used to acquire a token for a downstream API and store it in the MSAL.NET user token cache so that other parts of the web API can later call on the overrides of AcquireTokenOnSilent to call the downstream APIs. This call has the effect of refreshing the tokens, if needed.

private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityToken jwtToken, ClaimsPrincipal principal, HttpContext httpContext)
{
 try
 {
  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 = application.AcquireTokenOnBehalfOf(requestedScopes.Except(scopesRequestedByMsalNet),
                                                  userAssertion)
                                        .ExecuteAsync()
                                        .GetAwaiter().GetResult();
 }
 catch (MsalException ex)
 {
  Debug.WriteLine(ex.Message);
  throw;
 }
}

Protocol

For more information about the on-behalf-of protocol, see Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow

Next steps