Search code examples
asp.net-mvc-5microsoft-graph-apiazure-ad-msal.net-4.8

Microsoft Graph returning null account even after passing a valid account ID


I am encountering a weird issue with Microsoft Graph on an integration that was built a few years back.

This issue started happening a few months back. After I sync a Microsoft Account and provide email and calendar read/write access, everything works fine for some time. I am able to retrieve emails and calendar events. However, after some time, I notice that when a call is made to GetAccountAsync with a valid AccountID, null is returned. This is causing AcquireTokenSilent to fail with the following error:

  • Error Code: user_null
  • Error Message: No account or login hint was passed to the AcquireTokenSilent call.

I have also noticed that this happens under the following scenarios:

  1. When the WebJob (console app) is run every 15 minutes, I encounter this issue
  2. To narrow down the root cause, I have deleted the WebJob to see if the issue occurs on the web app. It looks like the issue starts to occur after an hour or so even without the web job running.

I have upgraded to the latest version of MSAL and implemented 4.46.1.0 version of Microsoft.Identity.Client. I am using .NET Framework 4.8 and this is a .NET MVC 5 app.

Here's my code:

public async Task<string> GetAccessTokenAsync()
{
    string accessToken;
    UserExternalApp.Scope = string.IsNullOrWhiteSpace(UserExternalApp.Scope) ? "" : UserExternalApp.Scope;

    // Load the app config from web.config
    var microsoftScopes = UserExternalApp.Scope.Replace(' ', ',').SplitAndTrim(new char[] { ',' }).ToList();
    var accountID = UserExternalApp.ExternalUserAccountID;

    var app = ConfidentialClientApplicationBuilder.Create(ClientID)
        .WithRedirectUri(DefaultRedirectUrl) // https:\//mywebsite.com
        .WithClientSecret(Secret)
        .Build();

    app.AddDistributedTokenCache(services =>
    {
        services.AddDistributedSqlServerCache(options =>
        {
            options.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;
            options.SchemaName = "dbo";
            options.TableName = "TokenCache";
            options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
        });
    });

    try
    {
        var account = await app.GetAccountAsync(accountID);
        var query = app.AcquireTokenSilent(microsoftScopes, account); // This is where the error is thrown
        var acquireTokenSilent = await query.ExecuteAsync();

        accessToken = acquireTokenSilent.AccessToken;
    }
    catch
    {
        // This is the error thrown:
        // Exception Type: MsalUiRequiredException
        // Error code: user_null
        // Exception Details: No account or login hint was passed to the AcquireTokenSilent call.  
        throw;
    }
    return accessToken;
}

I know the token is persisted on my SQL Server:

enter image description here


Solution

  • I would like to share the resolution to this problem in case if it helps someone in the future. I feel that this is a Microsoft Bug that was introduced during one of their many upgrade process as this code went from working to broken without any change from our end. Here are the steps I took:

    1. While exchanging the code for a token after user authentication, I retrieved and saved Account.HomeAccountId.Identifier, Account.HomeAccountId.ObjectId and TenantId for the account.
    2. I implemented my own version of IAccount.
    3. Instead of calling await app.GetAccountAsync(accountID), I used my implementation of IAccountand initialized it with the data I saved in Step 1.
    4. I used this account to call app.AcquireTokenSilent(microsoftScopes, account).

    And that's it! No error was thrown once this was done!