Skip to content

Multi tenant client_credential use

Bogdan Gavril edited this page Nov 25, 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

// 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 - use MSAL's internal memory cache (AcquireTokenForClient)

This does not scale well for web sites or web apis that deal with users (!). It should only be used with AcquireTokenForClient (i.e. app to app) only.

static async Task<string> GetTokenAsync
{    
    var cca = ConfidentiClientApplicationBuilder
              .WithCertificate(x509certificate)
              .WithAuthority($"https://login.microsoftonline.com/{tenant_id}") // do not use common, unless you override the tenant at the request level
              .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) // cache is shared between all ConfidentiClientApplication objects
              .Create("client_id"); 

    var result = await cca .AcquireTokenForClient("scope_for_downstream_api")
                      .WithTenantId(tid) // optional, if you want to reuse the cca object
                      .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.
  • In AcquireTokenForClient(), the number of tokens = number of tenants x number of resources you need to access. There are some ~1M tenants in AAD, and apps typically access a single resource in each one. So the token cache does not grow beyond ~2-3GB of memory.
  • Can also be used by web sites / web apis that see little traffic.
  • MSAL 4.37+ only
  • If you need to use different client IDs, then maintain a dictionary of <client_id> -> ConfidentialClientApplication

Pattern 2 - L2 caching

L2 caching works for user flows (authorization code and OBO).

// 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;

}
  • L2 cachingserializes / deserializes the cache using JSON, so it is slower than in-memory caching.
  • 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