Account not found after restart: No account or login hint was passed to the AcquireTokenSilent call

Viewed 2351

I have a web application that must send a B2C Bearer token to the API in order to get authorization. I achieve this by using MSAL, everything works great till I restart the web application, as soon as I restart the web application (without Logging Out) it seems that the B2C claims are still found by the application and the user is still logged, but IConfidentialClientApplication cannot use the GetAccountAsync() because there is no account to be found.

The Error I get is: {"No account or login hint was passed to the AcquireTokenSilent call."}

The problem is resolved if the user Signs Out. This happens on the localhost and also happens if the application is published with Azure and I restart the app service.

3 Answers

Just as juunas explained in comment, MSAL uses an in memory token cache by default.

Once the client logins, authentication information will be stored in cookie(if cookie is not disabled). Even your web application restarts, the client will keep logged in.

However, as in memory cache is used, all the cached items will be cleaned if application restarts. To solve this, you can custom token cache serialization in MSAL.NET.

You can create an exception filter to handle this for you automatically. One example is this AuthorizeForScopeSttribute class.

The user wont be signed out, but the token cache will get initialized.

Well, i ran into the same problem as @andrei0809, and was able to solve my problem by following the advice of @juunas and @JackJia.

I gave a point to @JackJia for pointing to the right place. I will just add some code here for the sake of completion.


Disclaimer

Before i start i will just mention that in my case i am working in a Public Client Application and not in a Confidential Client Application as the OP. Serialization as recommended in the MS documentation is a bit different for both cases, so i will add both below even when i only used that for Public Client Applications.

Why is Custom token serialization not made out of the box in MSAL.NET?

Going back to the docu we learn that:

.Net desktop and .Net core applications, on the other hand, have varied architectures, and MSAL cannot implement a serialization mechanism that fits all purposes

Further, it is also important to note that the feature IS default for mobile platforms (UWP, Xamarin).

Persisting Token Cache in public client applications

First of all, you have to create a helper class that take an ITokenCache object and uses its SetBeforeAccess and SetAfterAccess methods to set delegates that will retrieve and update the token cache before and after access respectively:

static class TokenCacheHelper
 {
  public static void EnableSerialization(ITokenCache tokenCache)
  {
   tokenCache.SetBeforeAccess(BeforeAccessNotification);
   tokenCache.SetAfterAccess(AfterAccessNotification);
  }

  /// <summary>
  /// Path to the token cache
  /// </summary>
  public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";

  private static readonly object FileLock = new object();


  private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
  {
   lock (FileLock)
   {
    args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
            ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                      null,
                                      DataProtectionScope.CurrentUser)
            : null);
   }
  }

  private static void AfterAccessNotification(TokenCacheNotificationArgs args)
  {
   // if the access operation resulted in a cache update
   if (args.HasStateChanged)
   {
    lock (FileLock)
    {
     // reflect changesgs in the persistent store
     File.WriteAllBytes(CacheFilePath,
                         ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
                                                 null,
                                                 DataProtectionScope.CurrentUser)
                         );
    }
   }
  }
 }

Then, when you have your helper class all that is left is to use it to "EnableSerialization" on your UserTokenCache object:

app = PublicClientApplicationBuilder.Create(ClientId)
    .Build();
TokenCacheHelper.EnableSerialization(app.UserTokenCache);

Persisting Token Cache in confidential client applications

(Since this is not the approach i needed/used, i will just directly refer to the docu)

In the case of Web Apps or Web APIs, the cache can be very different, leveraging the session, or a Redis cache, or a database.

A very important thing to remember is that for Web Apps and Web APIs, there should be one token cache per user (per account). You need to serialize the token cache for each account.

Examples of how to use token caches for Web apps and Web APIs are available in the ASP.NET Core Web app tutorial in the phase 2-2 Token Cache. For implementations have a look at the following folder TokenCacheProviders in the microsoft-authentication-extensions-for-dotnet library (in the Microsoft.Identity.Client.Extensions.Web folder. This library could be available in the future as a NuGet package. Token cache for a daemon app

When acquiring a token for a service principal, i.e. on behalf of an application, you use the Confidential Client grant.

In this case, the token issuer (AAD), only emits Access Tokens. IDTokens are not created because ID Tokens are related to users. Refresh Tokens are not created for security reasons. The structure of the token cache is different, as it only focuses on access tokens, which anyway have short expiration.

To serialize the content of this cache:

ConfidentialClientApplicationBuilder
       .Create(s_clientIdForConfidentialApp)
       .WithClientSecret(s_confidentialClientSecret)
       .Build();

// Instruct MSAL how to serialise the cache, but use AppTokenCache
instead of the UserTokenCache
cca.AppTokenCache.SetBeforeAccess(notificationArgs => ...);
cca.AppTokenCache.SetAfterAccess(notificationArgs => ...);

var result = await cca.AcquireTokenForClientAsync(...) ```

where, although not tried, i belive you can complete the callbacks with the code for AfterAccessNotification and BeforeAccessNotification above in the Public Client Application section.

Hope this helps someone there facing the same problem i was facing!

Related