Skip to content

Multi tenant client_credential use

Jean-Marc Prieur edited this page Jul 21, 2021 · 27 revisions

Pattern for using MSAL for client credential flow in multi-tenant services

Decision point - Microsoft.Identity.Web or Microsoft.Identity.Client (MSAL)?

If you use ASP.NET Core, you are encouraged to adopt Microsoft.Indentity.Web, which provides a higher level API over token acquisition and has better defaults. See Is MSAL.NET right for me?

Decision point - token caching

MSAL maintains a token cache which grows with each token acquired. MSAL manages token lifetimes in a smart way, so you should use its cache. If your service needs to call N tenants, there will be potentially N tokens in MSAL's cache, each around 2Kb in size. N can be very large, there are > 1 million tenants in AAD and this number is growing.

Anti-pattern - do it yourself

// Problem: cca goes out of scope and MSAL's internal cache is lost
string GetAccessToken()
{
      // each ConfidentiClientApplication object maintains its own internal cache 
      ConfidentiClientApplication cca = ConfidentiClientApplicationBuilder
              .WithCertificate(x509certificate)
              .Create("client_id"); 

     var result = cca.AcquireTokenForClient().WithSendX5C(true) // for SNI
                      .ExecuteAsync();
}

If you keep calling this GetAccessToken above, you'll always make an HTTP request to AAD. If you manage the token expiry on your own, you'll be missing out on the pro-active refresh feature MSALs implement. With this feature, services receive tokens available for a long time (12h) and are instructed to refresh them for half that time (6h). This ensures that even if AAD / ESTS goes down, your service has a fresh token that is available for a long time.

Pattern 1 - let MSAL do it

Note: does not work for OBO, use pattern 2 instead

// When your service starts, create a singleton / static ConfidentialClientApplication
static ConfidentiClientApplication s_cca;
static void Init()
{    
    s_cca = ConfidentiClientApplicationBuilder
              .WithCertificate(x509certificate)
              // don't worry about the tenant here, you'll specify it in the request; but the app and the request must target the same cloud ! (authority env)
              .WithAuthority("https://login.microsoftonline.com/common") 
              .Create("client_id"); 
}

// Then, whenever you need a token, specify the authority
async string GetAccessToken()
{
    var result = await s_cca .AcquireTokenForClient("scope_for_downstream_api")
                      .WithAuthority("https://login.microsoftonline.com/<tenant_id>") // do NOT use common or organizations here
                      .WithSendX5C(true) // for SNI
                      .ExecuteAsync();

   // You can monitor if the cache was hit
   bool cacheHit = result.AuthenticationResult.AuthenticationResultMetadata.TokenSource == TokenSource.Cache;

   return result.AccessToken;
}
  • MSAL does not evict items from the cache, and at 2KB size per token, you could eventually go out of memory.
  • MSAL 4.30+ only
  • But if you are migrating from ADAL and haven't run out of memory, this will have the same memory profile.
  • If you need to use different client IDs, then maintain a dictionary of <client_id> -> ConfidentialClientApplication

Pattern 2 - take control of the cache

This is the North Star and works for OBO as well.

// In your app initialization define a cache 
// See https://github.com/Azure-Samples/active-directory-dotnet-v1-to-v2/blob/master/ConfidentialClientTokenCache/Program.cs#L83 for several implementations 
static var s_cache = InMemoryWithLRU / Redis / SqlServer / L1InMemroy_L2Distributed / etc.

// Then when you need a token
async string GetAccessToken()
{
     var cca = ConfidentialClientApplicationBuilder.Create("client_id")
               .WithAuthority("https://login.microsoftonline.com/<tenantid>")
               .WithCertificate(certificate)
               .Build();

    s_cache.Initialize(cca.AppTokenCache); // configure the CCA to use your token cache

    var result = await app.AcquireTokenForClient(new[] {"https://graph.windows.net/.default"})
                 .WithSendX5C(true) // SNI
                 .ExecuteAsync();

   // You can monitor if the cache was hit
   bool cacheHit = result.AuthenticationResult.AuthenticationResultMetadata.TokenSource == TokenSource.Cache;

   return result.AccessToken;

}
  • Microsoft.Identity.Web project has several high performance token cache implementations.
  • If not using ASP.NET Core, see this simple sample that shows how to use a token cache from Microsoft.Identity.Web in ANY application
  • Distributed apps are encouraged to use an L1 / L2 cache, where L2 is distributed and shared between all pods (e.g. Redis). See L1/L2 cache.

Getting started with MSAL.NET

Acquiring tokens

Desktop/Mobile apps

Web Apps / Web APIs / daemon apps

Advanced topics

News

FAQ

Other resources

Clone this wiki locally