I found the problem - it was the need to store the token in a cache file. When I did a Google search for msalcache, it came back as the TokenCacheHelper, which is in the stack trace. This file appears to be auto-generated with the code output below.
//------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//------------------------------------------------------------------------------
using Microsoft.Identity.Client;
using System.IO;
using System.Runtime.Versioning;
using System.Security.Cryptography;
namespace <AppName>.Helpers
{
static class TokenCacheHelper
{
/// <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();
public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
null,
DataProtectionScope.CurrentUser)
: null);
}
}
public 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)
);
}
}
}
internal static void EnableSerialization(ITokenCache tokenCache)
{
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
}
}
After doing some more searching, I found these two links of relevance:
The relevant code in question is for the CacheFilePath, which is actually stored in a comment:
/// <summary>
/// Path to the token cache. Note that this could be something different for instance for MSIX applications:
/// private static readonly string CacheFilePath =
/// $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\{AppName}\msalcache.bin";
/// </summary>
public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";
The recommended fix for the CacheFilePath is actually invalid. So, I made the following modification:
private static readonly string AppName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
private static readonly string ApplicationDataFolder = $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\{AppName}\\";
private static readonly string CacheFilePath = $"{ApplicationDataFolder}\\msalcache.bin";
I then added the following method:
public static void CreateApplicationDataDirectory()
{
FileInfo fileInfo = new FileInfo(ApplicationDataFolder);
// Check to see if the directory exists. If it does not then create it. If we do not do this then the token CacheFilePath will
// not be created.
if (!fileInfo.Exists)
Directory.CreateDirectory(fileInfo.Directory.FullName);
}
I then modified the App.Xaml.cs file to call the CreateApplicationDataDirectory right after the ApplicationBuild process:
_clientApp = PublicClientApplicationBuilder.Create(Params.ClientId)
.WithAuthority(AzureCloudInstance.AzurePublic, Params.Tenant)
.WithRedirectUri("http://localhost:1234")
.Build();
TokenCacheHelper.CreateApplicationDataDirectory();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);