Search code examples
wpfazuresingle-sign-on.net-5msix

MSIX Windows 10 App Access Denied post-SSO sign-in for Azure migration


We are in the process of migrating our applications into Azure.

We have created an MSIX installer for an internal WPF application that installs to the Windows 10 C:\Program Files\WindowsApps\ folder. When we run the application, it requires us to enter our Single Sign-On (SSO) credentials via the online Windows/Azure web portal. After successfully entering our credentials, we get a following pop-up that says access to a file is denied (see below). We get this error regardless of whether running it normally or as administrator.

We are unable to find anything online that has been helpful in resolving the error. We did try taking ownership of this protected folder and then unchecking the read-only option, but that did not work (nor does that sound like a good idea, but this is troubleshooting). We do not see anything in the MSIX setup project that can resolve this issue. Does anyone know why we are getting this error and how to resolve it?

Error Message Box

In the Event Viewer, the following information is provided:

Event Viewer Error Details


Solution

  • 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);