MSIX Windows 10 个应用程序访问被拒绝 post-Azure 迁移的 SSO 登录

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

我们正在将我们的应用程序迁移到 Azure。

我们已经为安装到 Windows10C:\Program Files\WindowsApps\ 文件夹的内部 WPF 应用程序创建了一个 MSIX 安装程序。当我们 运行 应用程序时,它要求我们通过在线 Windows/Azure 网络门户输入我们的单点登录 (SSO) 凭据。成功输入我们的凭据后,我们会看到一个弹出窗口,提示对文件的访问被拒绝(见下文)。无论 运行 正常还是 as administrator.

我们都会收到此错误

我们在网上找不到任何有助于解决错误的信息。我们确实尝试获取此受保护文件夹的所有权,然后取消选中只读选项,但这没有用(这听起来也不是个好主意,但这是故障排除)。我们在 MSIX 安装项目中看不到任何可以解决此问题的内容。有谁知道我们为什么会收到此错误以及如何解决它?

在事件查看器中,提供了以下信息:

MSIX 打包的应用程序不支持对安装文件夹 (WindowsApps) 中文件的写入权限。这是设计使然,OS.

不允许取得所有权并强制更改文件

应用程序 运行ning 时需要更改(更新)的任何配置文件都应保存在 AppData 用户文件夹或 CommonApplicationData 计算机文件夹中(如果多个用户需要访问此 config/file).

我已经在这里写了一篇关于这个主题的更长的解释:

P.S。一般而言,将用户数据文件保存在安装文件夹中是一种不好的做法,即使您不使用 MSIX 部署应用程序也是如此。应用程序文件和应用程序数据(配置、用户数据等)之间的逻辑分离有助于确保您的客户可以顺利升级您的应用程序而不会丢失他们的 data/configs,如果旧电脑崩溃,可以轻松迁移到新电脑,可以 运行 虚拟环境(企业客户端)中的应用程序,并且您不会因为支持而头疼;)。

We are unable to find anything online that has been helpful in resolving the error.

来自 docs 列出了将现有安装程序转换为 MSIX 之前需要了解的事项:

Your application writes to the install directory for your app. For example, your application writes to a log file that you put in the same directory as your exe. This isn't supported because the folder is protected. We recommend writing to another location like the local app data store. We have added a capability that allows this in 1809 and later.

解决方案显然是将文件保存在您的应用程序对其具有写入权限的另一个文件夹中。 MSIX 打包应用永远无法将文件写入安装文件夹。

我发现了问题 - 需要将令牌存储在缓存文件中。当我执行 Google 搜索 msalcache 时,它返回为 TokenCacheHelper,它在堆栈跟踪中。此文件似乎是使用下面的代码输出自动生成的。

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

经过更多搜索,我找到了这两个相关链接:

有问题的相关代码是针对CacheFilePath的,实际上是存放在注释中:

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

针对 CacheFilePath 的建议修复实际上无效。所以,我做了如下修改:

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

然后我添加了以下方法:

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

然后我修改了 App.Xaml.cs 文件以在 ApplicationBuild 过程之后立即调用 CreateApplicationDataDirectory

_clientApp = PublicClientApplicationBuilder.Create(Params.ClientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, Params.Tenant)
            .WithRedirectUri("http://localhost:1234")
            .Build();
TokenCacheHelper.CreateApplicationDataDirectory();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);