运行自动集成测试

作为开发人员,你希望对开发的应用运行自动集成测试。 在自动集成测试中调用受 Microsoft 标识平台保护的 API(或其他受保护的 API,例如 Microsoft Graph)是一项难题。 Microsoft Entra ID 通常需要交互式用户登录提示,这很难自动执行。 本文介绍如何使用名为资源所有者密码凭据授予 (ROPC) 的非交互式流程自动登录用户,以便进行测试。

若要准备自动集成测试,请创建一些测试用户,创建和配置应用注册,并对租户进行一些潜在的配置更改。 其中一些步骤需要管理员权限。 此外,Microsoft 建议不要在生产环境中使用 ROPC 流。 创建由你作为管理员的单独测试租户,以便能够安全有效地运行自动集成测试。

警告

Microsoft 建议不要在生产环境中使用 ROPC 流。 在大多数生产情况下,提供并建议使用更安全的替代项。 在应用程序中,ROPC 流需要非常高的信任度,并携带其他流中不存在的身份验证风险。 只应在单独的测试租户中使用此流进行测试,并且只能用于测试用户。

重要

  • Microsoft 标识平台仅支持将 ROPC 用于 Microsoft Entra 租户而非个人帐户。 这意味着,必须使用特定于租户的终结点 (https://login.microsoftonline.com/{TenantId_or_Name}) 或 organizations 终结点。
  • 受邀加入 Microsoft Entra 租户的个人帐户不能使用 ROPC。
  • 没有密码的帐户无法使用 ROPC 登录,这意味着 SMS 登录、FIDO 等功能以及 Authenticator 应用都不可用于该流。
  • 如果用户需使用多重身份验证 (MFA) 来登录应用程序,则系统会改为阻止用户。
  • 混合联合身份验证方案(例如,用于对本地帐户进行身份验证的 Microsoft Entra ID 和 Active Directory 联合身份验证服务 (AD FS))不支持 ROPC。 如果用户被整页重定向到本地标识提供程序,Microsoft Entra ID 无法针对该标识提供程序测试用户名和密码。 不过,ROPC 支持直通身份验证
  • 混合联合身份身份验证方案的一种例外情况如下:当本地密码同步到云时,将 AllowCloudPasswordValidation 设置为 TRUE 时,Home Realm Discovery 策略将启用 ROPC 流来处理联合用户。 有关详细信息,请参阅为旧版应用程序启用对联合用户的直接 ROPC 身份验证

创建单独的测试租户

在生产环境中使用 ROPC 身份验证流程存在风险,因此请创建单独的租户来测试应用程序。 可以使用现有测试租户,但你需要是租户中的管理员,因为以下步骤需要管理员权限。

创建和配置密钥保管库

建议将测试用户名和密码作为机密安全地存储在 Azure Key Vault 中。 稍后运行测试时,测试会在安全主体的上下文中运行。 如果在本地运行测试(例如在 Visual Studio 或 Visual Studio Code 中),则安全主体是 Microsoft Entra 用户;如果在 Azure Pipelines 或其他 Azure 资源中运行测试,则安全主体是服务主体或托管标识。 安全主体必须具有 Read 和 List 机密权限,以便测试运行程序可从你的密钥保管库中获取测试用户名和密码。 有关详细信息,请参阅 Azure Key Vault 中的身份验证

  1. 如果还没有密钥保管库,请创建新的密钥保管库
  2. 请记下 Vault URI 属性值(类似于 https://<your-unique-keyvault-name>.vault.azure.net/),本文后面的示例测试中会使用该值。
  3. 为运行测试的安全主体分配访问策略。 向用户、服务主体或托管标识授予密钥保管库中的 Get 和 List 机密权限。

创建测试用户

提示

本文中的步骤可能因开始使用的门户而略有不同。

在租户中创建一些测试用户以进行测试。 由于测试用户不是真人,我们建议你分配复杂的密码并将这些密码作为机密安全地存储在 Azure Key Vault 中。

  1. 至少以云应用程序管理员身份登录到 Microsoft Entra 管理中心
  2. 浏览到“标识”>“用户”>“所有用户”。
  3. 选择“新建用户”,并在目录中创建一个或多个测试用户帐户。
  4. 本文稍后的示例测试使用单个测试用户。 在之前创建的密钥保管库中将测试用户名和密码添加为机密。 将用户名添加为名为“TestUserName”的机密,将密码添加为名为“TestPassword”的机密。

创建和配置应用注册

注册一个应用程序,用于在测试期间调用 API 时充当客户端应用。 该应用程序不应是生产环境中已有的同一应用程序。 应该有单独的应用,仅用于测试目的。

注册应用程序

创建应用注册。 可以按照应用注册快速入门中的步骤进行操作。 无需添加重定向 URI 或添加凭据,因此可以跳过这些部分。

记下应用程序(客户端)ID,本文后面的示例测试中会使用该 ID。

为公共客户端流启用应用

ROPC 是公共客户端流,因此需要为公共客户端流启用应用。 从 Microsoft Entra 管理中心内你的应用注册,转到“身份验证”>“高级设置”>“允许公共客户端流”。 将切换开关设置为“是”。

由于 ROPC 不是交互式流,因此不会在运行时通过同意屏幕提示你同意这些权限。 预先同意权限,以免获取令牌时出错。

向应用添加权限。 不要向应用添加任何敏感权限或高特权权限,建议将测试方案范围限定为与 Microsoft Entra ID 集成相关的基本集成方案。

Microsoft Entra 管理中心内你的应用注册,转到“API 权限”>“添加权限”。 添加调用你要使用的 API 所需的权限。 本文中的进一步测试示例会使用 https://graph.microsoft.com/User.Readhttps://graph.microsoft.com/User.ReadBasic.All 权限。

添加权限后,你需要同意这些权限。 同意权限的方式取决于测试应用是否与应用注册在同一租户中,以及你是否为租户中的管理员。

应用和应用注册在同一租户中,并且你是管理员

如果你计划在注册应用的同一租户中测试应用,并且你是该租户的管理员,则可以通过 Microsoft Entra 管理中心同意权限。 在 Azure 门户的应用注册中,转到“API 权限”并选择“添加权限”按钮旁边的“为 <your_tenant_name> 授予管理员同意”按钮,然后按“是”确认。

应用和应用注册在不同租户中,或者你不是管理员

如果你没有计划在注册应用的同一租户中测试应用,或者你不是该租户的管理员,则无法通过 Microsoft Entra 管理中心同意权限。 但是,你仍可以通过在 Web 浏览器中触发登录提示来同意某些权限。

Microsoft Entra 管理中心内的你应用注册中,转到“身份验证”>“平台配置”>“添加平台”>“Web”。 添加重定向 URI "https://localhost";然后选择“配置”。

非管理员用户无法通过 Azure 门户预先同意,因此请在浏览器中发送以下请求。 出现登录屏幕提示时,请使用前面一步中创建的测试帐户登录。 同意系统提示的相关权限。 可能需要对要调用的每个 API 重复此步骤,并测试要使用的用户。

// Line breaks for legibility only

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id={your_client_ID}
&response_type=code
&redirect_uri=https://localhost
&response_mode=query
&scope={resource_you_want_to_call}/.default
&state=12345

将 {tenant} 替换为你的租户 ID,将 {your_client_ID} 替换为应用程序的客户端 ID,将 {resource_you_want_to_call} 替换为标识符 URI(例如 "https://graph.microsoft.com")或要尝试访问的 API 的应用 ID。

从 MFA 策略中排除测试应用和用户

你的租户可能具有条件访问策略,该策略要求所有用户进行多重身份验证 (MFA),如 Microsoft 建议的一样。 MFA 不适用于 ROPC,因此需要对测试应用和测试用户免除这项要求。

若要排除用户帐户,请执行以下操作:

  1. 至少以云应用程序管理员身份登录到 Microsoft Entra 管理中心
  2. 在左侧导航窗格中浏览至“标识”>“安全中心”,然后选择“条件访问”。
  3. 在“策略”中,选择需要 MFA 的条件访问策略。
  4. 选择“用户或工作负载标识”。
  5. 选择“排除”选项卡,然后选择“用户和组”复选框。
  6. 在“选择排除的用户”中选择要排除的用户帐户。
  7. 选择“选择”按钮,然后选择“保存”。

若要排除测试应用程序,请执行以下操作:

  1. 在“策略”中,选择需要 MFA 的条件访问策略。
  2. 选择“云应用或操作”。
  3. 选择“排除”选项卡,然后选择“选择排除的云应用”。
  4. 在“选择排除的云应用”中选择要排除的应用。
  5. 选择“选择”按钮,然后选择“保存”。

编写应用程序测试

你现在已设置完成,可以编写自动测试。 下面是针对以下对象的测试:

  1. .NET 示例代码使用 Microsoft 身份验证库 (MSAL)xUnit(一种常见的测试框架)。
  2. JavaScript 示例代码使用 Microsoft 身份验证库 (MSAL)Playwright(一种常见的测试框架)。

设置 appsettings.json 文件

将之前创建的测试应用的客户端 ID、必需的范围以及密钥保管库 URI 添加到测试项目的 appsettings.json 文件中。

{
  "Authentication": {
    "AzureCloudInstance": "AzurePublic", //Will be different for different Azure clouds, like US Gov
    "AadAuthorityAudience": "AzureAdMultipleOrgs",
    "ClientId": <your_client_ID>
  },

  "WebAPI": {
    "Scopes": [
      //For this Microsoft Graph example.  Your value(s) will be different depending on the API you're calling
      "https://graph.microsoft.com/User.Read",
      //For this Microsoft Graph example.  Your value(s) will be different depending on the API you're calling
      "https://graph.microsoft.com/User.ReadBasic.All"
    ]
  },

  "KeyVault": {
    "KeyVaultUri": "https://<your-unique-keyvault-name>.vault.azure.net//"
  }
}

将客户端设置为在所有测试类中使用

使用 SecretClient() 从 Azure Key Vault 获取测试用户名和密码机密。 该代码会使用指数退避,以便在 Key Vault 受到限制的情况下进行重试。

DefaultAzureCredential() 通过从环境变量配置的服务主体获取的访问令牌或通过托管标识(如果代码在具有托管标识的 Azure 资源上运行)向 Azure Key Vault 进行身份验证。 如果代码在本地运行,DefaultAzureCredential 便会使用本地用户的凭据。 请在 Azure Identity 客户端库内容中了解详细信息。

使用 Microsoft 身份验证库 (MSAL) 来通过 ROPC 流进行身份验证并获取访问令牌。 访问令牌在 HTTP 请求中作为持有者令牌进行传递。

using Xunit;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using System.Security;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Extensions.Configuration;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Azure.Core;
using System;

public class ClientFixture : IAsyncLifetime
{
    public HttpClient httpClient;

    public async Task InitializeAsync()
    {
        var builder = new ConfigurationBuilder().AddJsonFile("<path-to-json-file>");

        IConfigurationRoot Configuration = builder.Build();

        var PublicClientApplicationOptions = new PublicClientApplicationOptions();
        Configuration.Bind("Authentication", PublicClientApplicationOptions);
        var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(PublicClientApplicationOptions)
            .Build();

        SecretClientOptions options = new SecretClientOptions()
        {
            Retry =
                {
                    Delay= TimeSpan.FromSeconds(2),
                    MaxDelay = TimeSpan.FromSeconds(16),
                    MaxRetries = 5,
                    Mode = RetryMode.Exponential
                 }
        };

        string keyVaultUri = Configuration.GetValue<string>("KeyVault:KeyVaultUri");
        var client = new SecretClient(new Uri(keyVaultUri), new DefaultAzureCredential(), options);

        KeyVaultSecret userNameSecret = client.GetSecret("TestUserName");
        KeyVaultSecret passwordSecret = client.GetSecret("TestPassword");

        string password = passwordSecret.Value;
        string username = userNameSecret.Value;
        string[] scopes = Configuration.GetSection("WebAPI:Scopes").Get<string[]>();
        SecureString securePassword = new NetworkCredential("", password).SecurePassword;

        AuthenticationResult result = null;
        httpClient = new HttpClient();

        try
        {
            result = await app.AcquireTokenByUsernamePassword(scopes, username, securePassword)
                .ExecuteAsync();
        }
        catch (MsalException) { }

        string accessToken = result.AccessToken;
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
    }

    public Task DisposeAsync() => Task.CompletedTask;
}

在测试类中使用

以下示例是调用 Microsoft Graph 的测试。 可将此测试替换为要在自己的应用程序或 API 上测试的任何内容。

public class ApiTests : IClassFixture<ClientFixture>
{
    ClientFixture clientFixture;

    public ApiTests(ClientFixture clientFixture)
    {
        this.clientFixture = clientFixture;
    }

    [Fact]
    public async Task GetRequestTest()
    {
        var testClient = clientFixture.httpClient;
        HttpResponseMessage response = await testClient.GetAsync("https://graph.microsoft.com/v1.0/me");
        var responseCode = response.StatusCode.ToString();
        Assert.Equal("OK", responseCode);
    }
}