运行自动集成测试

作为开发人员,你希望对开发的应用运行自动集成测试。 在自动集成测试中调用受 Microsoft 身份平台(或其他受保护的 API,例如 Microsoft Graph)保护的 API 是一项挑战。 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)登录应用程序,则他们将会被阻止。
  • 混合标识联合方案中不支持 ROPC(例如,Microsoft Entra ID 和 Active Directory 联合身份验证服务(AD FS)用于对本地帐户进行身份验证)。 如果用户被整页重定向到本地标识提供程序,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 属性值(类似于 ),本文后面的示例测试中会使用该值。
  3. 为运行测试的安全主体分配访问策略。 向用户、服务主体或托管标识授予密钥保管库中的 Get 和 List 机密权限。

创建测试用户

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

  1. 以至少云应用程序管理员身份登录到 Microsoft Entra 管理中心
  2. 浏览到 Entra ID>用户
  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(例如 ")或要尝试访问的 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 标识客户端库 内容中阅读详细信息。

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