Azure SQL 通过 API 使用 MS Identity Platform 作为用户进行身份验证

Azure SQL authenticate as user via API with MS Identity Platform

我有一个 ASP.NET Core 3.1 Web 应用程序调用 ASP.NET Core 3.1 Web API,后者又访问 Azure SQL 数据库。通过 MSAL(Microsoft Identity Platform)提供身份验证 - 即使用相对较新的 Microsoft.Identity.WebMicrosoft.Identity.Web.UI 库。

目标是确保用户在 his/her 自己登录的情况下通过 API 从 SQL 中提取数据,从而实现行级安全、访问审计和其他好东西。

我已经成功地让登录过程适用于 Web 应用程序 - 通过它它获得了一个有效的访问令牌来访问 API 使用我在向 AD 注册后者时创建的范围.

当我 运行 API 和来自 Visual Studio 本地的应用程序时,一切都按预期工作 - 正确的访问令牌被提供给应用程序以访问 API,以及 API 访问 SQL - 在这两种情况下都在用户(即我的)身份下。

但是,当我将 API 发布到 Azure 上的 App Services 并从本地版本的 Web App 或它的 App-Services 托管版本访问它时,访问令牌API 获取访问权限 SQL 包含 API 的应用程序标识(系统分配的托管标识),而不是用户的标识。虽然我可以访问 SQL 作为应用程序,但这不是我们需要的。

Web 应用程序使用 ITokenAcquisitionGetAccessTokenForUserAsync 方法获取其访问令牌 - 将我为 API.

定义的单个范围作为参数

API 获取其令牌(以访问 SQL),如下所示:

var token = await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net", _tenantId)

...其中 _tenantId 是订阅的租户 ID。

我已将 SQL Azure 数据库“user_impersonation”API 权限添加到 API 的 AD 注册 - 但这没有帮助。顺便说一句,出于某种原因,Azure 将此权限的全名命名为 https://sql.azuresynapse.usgovcloudapi.net/user_impersonation - 这有点令人担忧,因为这只是一个位于英国的常规 Azure 帐户。

我发现了一些与此类似的 post,但主要针对旧版本的解决方案集。我希望避免必须将自己的代码编写到 post 令牌请求 - 这应该由 MSAL 库处理。

我是否应该在登录后以某种方式从 Web 应用程序单独请求 SQL 访问范围,或者 API 是否应该做一些不同的事情来获得 SQL识别用户的访问令牌?为什么在本地 运行ning 时完美运行?

这似乎应该是一个非常常见的用例(最常见的?),但它几乎没有记录 - 我发现的大多数文档仅涉及正在使用的应用程序标识,或者没有告诉您要做什么为这个特定的技术栈做。

我目前正在处理类似的问题,使用的是 Net5.0 Web 应用程序。它似乎在本地工作的原因是您使用可以访问 Azure SQL 的用户登录 Visual Studio,这些是您在 Db 中获得的权利。 IDE 使用这些凭据代替托管服务标识,后者在您将应用程序上传到 Azure 时使用。

如您所述,在应用程序注册中,您需要向应用程序授予 Azure SQL 数据库 user_impersonation.

权限

在您的代码中,您需要从 https://database.windows.net//.default 请求令牌(请注意 // 因为 v1 端点需要它)。通过引用 /.default,您正在请求您在应用程序注册门户中为该应用程序选择的所有权限。

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope

在 Startup.cs 中,您需要使用所需范围的 EnableTokenAcquisitionToCallDownstreamApi。

        services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
            .EnableTokenAcquisitionToCallDownstreamApi(new[]
               {"https://database.windows.net//.default"})
            // Adds the User and App InMemory Token Cache 
            .AddInMemoryTokenCaches();

        services.AddAuthorization(options =>
        {
            // By default, all incoming requests will be authorized according to the 
            // default policy
            options.FallbackPolicy = options.DefaultPolicy;
        });

        services.AddDbContext<MyDatabaseContext>(options =>
               options.UseSqlServer(
               Configuration.GetConnectionString("MyAzureConnection")));

        // The database interface
        services.AddScoped<ITodos, TodoData>();

        services.AddRazorPages()
            .AddRazorRuntimeCompilation()
            .AddMvcOptions(o => 
            {
                var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
                o.Filters.Add(new AuthorizeFilter(policy));
            })
            .AddMicrosoftIdentityUI();

您还需要使用 [AuthorizeForScopes(Scopes = new string[]{"https://database.windows.net//.default"}] 装饰您的控制器并包括该控制器所需的范围。对于 Razor,它位于页面模型的顶部,需要引用“using Microsoft.Identity.Web;”

namespace ToDoApp.Pages.Todos
{
    [AuthorizeForScopes(ScopeKeySection = "AzureSQL:BaseUrl")]
    public class CreateModel : PageModel

我在我的 appsettings.json 中使用一个部分作为范围并使用 ScopeKeySection:

检索它
  "AzureSQL": {
    "BaseUrl": "https://database.windows.net//.default",
    "Scopes": "user_impersonation"
  }

这向您展示了将它包含在 MVC、Razor 和 Blazor 中的位置:

https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-mvc-controllers

最后,您的 DbContext 需要一个可以从客户端应用程序传递给它的令牌(也许...)。

我现在就是这样做的

    public class MyDatabaseContext : DbContext
    {
        private readonly ITokenAcquisition _tokenAcquisition;

        public MyDatabaseContext (ITokenAcquisition tokenAcquisition,
                            DbContextOptions<MyDatabaseContext> options)
            : base(options)
        {

            _tokenAcquisition = tokenAcquisition;
            string[] scopes = new[]{"https://database.windows.net//.default"};

            var result = _tokenAcquisition.GetAuthenticationResultForUserAsync(scopes)
                            .GetAwaiter()
                            .GetResult();
            token = result.AccessToken;

            var connection = (SqlConnection)Database.GetDbConnection();
            connection.AccessToken = result.token;
        }

这是一个有缺陷的解决方案。如果我重新启动应用程序并尝试再次访问它,我会收到错误 Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user

好像跟TokenCache有关。如果我注销并再次登录或清除浏览器缓存,错误就会解决。我有一个解决方法,可以在失败时登录应用程序,但它有缺陷,因为我使用的是应用程序的凭据。

但是,它成功连接到 Azure SQL Db 作为用户而不是具有用户权限的 App。当我解决错误(或找到错误)时,我将更新此答案。

终于——成功了!最后,这是文档的关键部分:Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow - 关键点是:

  1. 该应用程序只需要令牌即可访问 API。
  2. 然后 API 代表通过第一个令牌识别的用户请求一个令牌,以访问 SQL。

关键是 - 由于 API 无法触发第二步的同意 window - 我不得不使用 Enterprise Applications[= Azure 门户中的 28=] 选项卡以预先授予 SQL.

的权限

所以好消息是它确实有效:也许对某些人来说这是显而易见的,但在我看来,我花了太长时间才找到这个问题的答案。我会在适当的时候写出关于如何做到这一点的更完整的解释,因为这不仅仅是我在努力解决这个问题。

坏消息是 - 在我的调查过程中 - 我发现 Azure B2C(这是我接下来需要添加的东西)不支持这个“代表”流程 - click here for details.太可惜了,因为我认为这是最明显的用例!哦,好吧,回到绘图板。