使用 Microsoft Graph 管理 Azure AD B2C

利用 Microsoft Graph,可以管理 Azure AD B2C 目录中的资源。 支持用于管理 Azure AD B2C 资源(包括用户、标识提供者、用户流、自定义策略和策略密钥)的下列 Microsoft Graph API 操作。 以下各部分中的每个链接对应于 Microsoft Graph API 参考中该操作的相应页面。

注意

还可以以编程方式创建 Azure AD B2C 目录本身,以及链接到 Azure 订阅的相应 Azure 资源。 此功能不是通过 Microsoft Graph API 公开的,而是通过 Azure REST API 公开的。 有关详细信息,请参阅 B2C 租户 - 创建

先决条件

  • 若要使用 MS Graph API 并与 Azure AD B2C 租户中的资源交互,需要进行应用程序注册来获取这样做的权限。 请执行注册 Microsoft Graph 应用程序一文中的步骤,以便创建供管理应用程序使用的应用程序注册。

用户管理

注意

Azure AD B2C 目前不支持对目录对象使用高级查询功能。 这意味着不支持 $count$search 查询参数和 Not (not)、Not equals (ne) 以及在 $filter 查询参数中以 (endsWith) 运算符结尾。 有关详细信息,请参阅 Microsoft Graph中的查询参数Microsoft Graph 中的高级查询功能

用户电话号码管理

可供用户使用短信、语音呼叫多重身份验证进行登录的电话号码。 有关详细信息,请参阅 Microsoft Entra 身份验证方法 API

请注意,列表操作仅返回已启用的电话号码。 应启用以下电话号码来与列表操作一起使用。

注意

表示形式正确的电话号码会以在国家/地区代码和电话号码之间加入空格的形式进行存储。 默认情况下,Azure AD B2C 服务目前不添加此空间。

Screenshot of the Authentication methods page for a sample user from the Azure portal. The text box for phone number is highlighted.

软件 OATH 令牌身份验证方法

软件 OATH 令牌是基于软件的数量生成器,它使用基于 OATH 时间一次的密码 (TOTP) 身份验证器应用进行多重身份验证。 使用 Microsoft Graph API 管理注册到用户的软件 OATH 令牌:

标识提供者

管理 Azure AD B2C 租户中的用户流可用的标识提供者

用户流 (beta)

配置用于注册、登录、组合式注册和登录、密码重置和配置文件更新的预先生成的策略。

自定义策略 (beta)

下列操作可用于管理你的 Azure AD B2C 信任框架策略(称为自定义策略)。

策略密钥 (beta)

Identity Experience Framework 存储着自定义策略中引用的机密,以在组件之间建立信任关系。 这些机密可以是对称或非对称密钥/值。 在 Azure 门户中,这些实体显示为策略密钥

Microsoft Graph API 中策略密钥的顶层资源是信任的框架密钥集。 每个密钥集都包含至少一个密钥。 若要创建密钥,请先创建一个空的密钥集,然后在密钥集中生成一个密钥。 你可以创建手动机密、上传证书或 PKCS12 密钥。 密钥可以是某个生成的机密、某个字符串,或者是你上传的某个证书。 如果密钥集具有多个密钥,则只有其中一个密钥处于活动状态。

信任框架策略密钥集

信任框架策略密钥

应用程序

应用程序扩展(目录扩展)属性

应用程序扩展属性也称为目录或 Microsoft Entra 扩展。 若要在 Azure AD B2C 中管理这些属性,请使用 identityUserFlowAttribute 资源类型及其关联的方法。

每位用户最多可以存储 100 个目录扩展值。 若要管理用户的目录扩展属性,请在 Microsoft Graph 中使用以下用户 API

  • 更新用户:在用户对象中写入或移除目录扩展属性值。
  • 获取用户:检索用户的目录扩展值。 默认情况下,将通过 beta 终结点返回属性,仅在 $select 上通过 v1.0 终结点返回。

对于用户流,可使用 Azure 门户管理这些扩展属性。 对于自定义策略,Azure AD B2C 在策略首次向扩展属性中写入值时替你创建属性。

注意

在 Microsoft Entra ID 中,将通过 extensionProperty 资源类型及其关联的方法管理目录扩展。 但是,由于它们通过不应更新的 b2c-extensions-app 应用在 B2C 中使用,因此将使用 identityUserFlowAttribute 资源类型及其关联的方法在 Azure AD B2C 中对其进行管理。

租户使用情况

使用获取组织详细信息 API 获取目录大小配额。 需要添加 $select 查询参数,如以下 HTTP 请求所示:

GET https://microsoftgraph.chinacloudapi.cn/v1.0/organization/organization-id?$select=directorySizeQuota

organization-id 替换为你的组织或租户 ID。

对上述请求的响应类似于以下 JSON 代码片段:

{
    "directorySizeQuota": {
        "used": 156,
        "total": 1250000
    }
}

审核日志

若要详细了解如何访问 Azure AD B2C 审核日志,请参阅访问 Azure AD B2C 审核日志

条件性访问

检索或还原已删除的用户和应用程序

只能恢复在最近 30 天内删除的用户和应用。

如何以编程方式管理 Microsoft Graph

可以通过两种方式管理 Microsoft Graph:

  • 委托的权限,用户或管理员同意应用请求的权限。 当应用调用目标资源时,它被委托了充当已登录用户的权限。
  • 应用程序权限由不需要登录用户在场的应用使用。 因此,只有管理员才能同意应用程序权限。

注意

通过用户流或自定义策略登录的用户的委托权限不能针对 Microsoft 图形 API 的委托权限使用。

代码示例:如何以编程方式管理用户帐户

此代码示例是一个 .NET Core 控制台应用程序,它使用 Microsoft Graph SDK 来与 Microsoft Graph API 交互。 其中的代码演示了如何调用 API 来以编程方式管理 Azure AD B2C 租户中的用户。 可以下载示例存档 (*.zip),在 GitHub 中浏览存储库,或克隆存储库:

git clone https://github.com/Azure-Samples/ms-identity-dotnetcore-b2c-account-management.git

获取代码示例后,根据环境对其进行配置,然后生成项目:

  1. Visual StudioVisual Studio Code 中打开项目。

  2. 打开 src/appsettings.json

  3. appSettings 部分,将 your-b2c-tenant 替换为租户的名称,将 Application (client) IDClient secret 替换为管理应用程序注册的值。 有关详细信息,请参阅注册 Microsoft Graph 应用程序

  4. 在存储库的本地克隆中打开控制台窗口,切换到 src 目录,然后生成项目:

    cd src
    dotnet build
    
  5. 使用 dotnet 命令运行应用程序:

    dotnet bin/Debug/netcoreapp3.1/b2c-ms-graph.dll
    

应用程序将显示可执行的命令列表。 例如,获取所有用户、获取单个用户、删除用户、更新用户的密码和批量导入。

注意

要使应用程序能够更新用户帐户密码,需要向应用程序授予用户管理员角色

代码探讨

示例代码使用 Microsoft Graph SDK,旨在简化可访问 Microsoft Graph 的优质、高效且可复原的应用程序的生成。

对 Microsoft Graph API 发出的任何请求都需要使用访问令牌进行身份验证。 该解决方案利用 Microsoft.Graph.Auth NuGet 包,该包提供 Microsoft 身份验证库 (MSAL) 的基于身份验证方案的包装器,以便与 Microsoft Graph SDK 配合使用。

Program.cs 文件中的 RunAsync 方法:

  1. appsettings.json 文件读取应用程序设置
  2. 使用 OAuth 2.0 客户端凭据授予流初始化身份验证提供程序。 应用可以使用客户端凭据授予流获取用于调用 Microsoft Graph API 的访问令牌。
  3. 使用身份验证提供程序设置 Microsoft Graph 服务客户端:
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using Microsoft.Graph;
using Azure.Identity;
using Microsoft.Identity.Client;
using System.Net.Http.Headers;

namespace b2c_ms_graph
{
    public static class Program
    {
        static async Task Main(string[] args)
        {
            //<ms_docref_set_auth_provider>
            // Read application settings from appsettings.json (tenant ID, app ID, client secret, etc.)
            AppSettings config = AppSettingsFile.ReadFromJsonFile();

            // Initialize the client credential auth provider
            var scopes = new[] { "https://microsoftgraph.chinacloudapi.cn/.default" };
            var clientSecretCredential = new ClientSecretCredential(config.TenantId, config.AppId, config.ClientSecret);
            var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
            //</ms_docref_set_auth_provider>
            PrintCommands();

            try
            {
                while (true)
                {
                    Console.Write("Enter command, then press ENTER: ");
                    string decision = Console.ReadLine();
                    switch (decision.ToLower())
                    {
                        case "1":
                            await UserService.ListUsers(graphClient);
                            break;
                        case "2":
                            await UserService.GetUserById(graphClient);
                            break;
                        case "3":
                            await UserService.GetUserBySignInName(config, graphClient);
                            break;
                        case "4":
                            await UserService.DeleteUserById(graphClient);
                            break;
                        case "5":
                            await UserService.SetPasswordByUserId(graphClient);
                            break;
                        case "6":
                            await UserService.BulkCreate(config, graphClient);
                            break;
                        case "7":
                            await UserService.CreateUserWithCustomAttribute(graphClient, config.B2cExtensionAppClientId, config.TenantId);
                            break;
                        case "8":
                            await UserService.ListUsersWithCustomAttribute(graphClient, config.B2cExtensionAppClientId);
                            break;
                        case "9":
                            await UserService.CountUsers(graphClient);
                            break;
                        case "help":
                            Program.PrintCommands();
                            break;
                        case "exit":
                            return;
                        default:
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("Invalid command. Enter 'help' to show a list of commands.");
                            Console.ResetColor();
                            break;
                    }

                    Console.ResetColor();
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"An error occurred: {ex}");
                Console.ResetColor();
            }
            Console.ReadLine();
        }

        private static void PrintCommands()
        {
            Console.ResetColor();
            Console.WriteLine();
            Console.WriteLine("Command  Description");
            Console.WriteLine("====================");
            Console.WriteLine("[1]      Get all users");
            Console.WriteLine("[2]      Get user by object ID");
            Console.WriteLine("[3]      Get user by sign-in name");
            Console.WriteLine("[4]      Delete user by object ID");
            Console.WriteLine("[5]      Update user password");
            Console.WriteLine("[6]      Create users (bulk import)");
            Console.WriteLine("[7]      Create user with custom attributes and show result");
            Console.WriteLine("[8]      Get all users (one page) with custom attributes");
            Console.WriteLine("[9]      Get the number of useres in the directory");
            Console.WriteLine("[help]   Show available commands");
            Console.WriteLine("[exit]   Exit the program");
            Console.WriteLine("-------------------------");
        }
    }
}

然后,在 UserService.cs 中使用初始化的 GraphServiceClient 来执行用户管理操作。 例如,获取租户中的用户帐户列表:

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Graph;

namespace b2c_ms_graph
{
    class UserService
    {
        
        //<ms_docref_get_list_of_user_accounts>
        public static async Task ListUsers(GraphServiceClient graphClient)
        {
            Console.WriteLine("Getting list of users...");

            try
            {
                // Get all users
                var users = await graphClient.Users
                    .Request()
                    .Select(e => new
                    {
                        e.DisplayName,
                        e.Id,
                        e.Identities
                    })
                    .GetAsync();

                // Iterate over all the users in the directory
                var pageIterator = PageIterator<User>
                    .CreatePageIterator(
                        graphClient,
                        users,
                        // Callback executed for each user in the collection
                        (user) =>
                        {
                            Console.WriteLine(JsonSerializer.Serialize(user));
                            return true;
                        },
                        // Used to configure subsequent page requests
                        (req) =>
                        {
                            Console.WriteLine($"Reading next page of users...");
                            return req;
                        }
                    );

                await pageIterator.IterateAsync();
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }
        //</ms_docref_get_list_of_user_accounts>

        public static async Task CountUsers(GraphServiceClient graphClient)
        {
            int i = 0;
            Console.WriteLine("Getting list of users...");

            try
            {
                // Get all users 
                var users = await graphClient.Users
                    .Request()
                    .Select(e => new
                    {
                        e.DisplayName,
                        e.Id,
                        e.Identities
                    })
                    .GetAsync();

                // Iterate over all the users in the directory
                var pageIterator = PageIterator<User>
                    .CreatePageIterator(
                        graphClient,
                        users,
                        // Callback executed for each user in the collection
                        (user) =>
                        {
                            i += 1;
                            return true;
                        },
                        // Used to configure subsequent page requests
                        (req) =>
                        {
                            Console.WriteLine($"Reading next page of users. Number of users: {i}");
                            return req;
                        }
                    );

                await pageIterator.IterateAsync();

                Console.WriteLine("========================");
                Console.WriteLine($"Number of users in the directory: {i}");
                Console.WriteLine("========================");
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }
        public static async Task ListUsersWithCustomAttribute(GraphServiceClient graphClient, string b2cExtensionAppClientId)
        {
            if (string.IsNullOrWhiteSpace(b2cExtensionAppClientId))
            {
                throw new ArgumentException("B2cExtensionAppClientId (its Application ID) is missing from appsettings.json. Find it in the App registrations pane in the Azure portal. The app registration has the name 'b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.'.", nameof(b2cExtensionAppClientId));
            }

            // Declare the names of the custom attributes
            const string customAttributeName1 = "FavouriteSeason";
            const string customAttributeName2 = "LovesPets";

            // Get the complete name of the custom attribute (Azure AD extension)
            Helpers.B2cCustomAttributeHelper helper = new Helpers.B2cCustomAttributeHelper(b2cExtensionAppClientId);
            string favouriteSeasonAttributeName = helper.GetCompleteAttributeName(customAttributeName1);
            string lovesPetsAttributeName = helper.GetCompleteAttributeName(customAttributeName2);

            Console.WriteLine($"Getting list of users with the custom attributes '{customAttributeName1}' (string) and '{customAttributeName2}' (boolean)");
            Console.WriteLine();

            // Get all users (one page)
            var result = await graphClient.Users
                .Request()
                .Select($"id,displayName,identities,{favouriteSeasonAttributeName},{lovesPetsAttributeName}")
                .GetAsync();

            foreach (var user in result.CurrentPage)
            {
                Console.WriteLine(JsonSerializer.Serialize(user));

                // Only output the custom attributes...
                //Console.WriteLine(JsonSerializer.Serialize(user.AdditionalData));
            }
        }

        public static async Task GetUserById(GraphServiceClient graphClient)
        {
            Console.Write("Enter user object ID: ");
            string userId = Console.ReadLine();

            Console.WriteLine($"Looking for user with object ID '{userId}'...");

            try
            {
                // Get user by object ID
                var result = await graphClient.Users[userId]
                    .Request()
                    .Select(e => new
                    {
                        e.DisplayName,
                        e.Id,
                        e.Identities
                    })
                    .GetAsync();

                if (result != null)
                {
                    Console.WriteLine(JsonSerializer.Serialize(result));
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }

        public static async Task GetUserBySignInName(AppSettings config, GraphServiceClient graphClient)
        {
            Console.Write("Enter user sign-in name (username or email address): ");
            string userId = Console.ReadLine();

            Console.WriteLine($"Looking for user with sign-in name '{userId}'...");

            try
            {
                // Get user by sign-in name
                var result = await graphClient.Users
                    .Request()
                    .Filter($"identities/any(c:c/issuerAssignedId eq '{userId}' and c/issuer eq '{config.TenantId}')")
                    .Select(e => new
                    {
                        e.DisplayName,
                        e.Id,
                        e.Identities
                    })
                    .GetAsync();

                if (result != null)
                {
                    Console.WriteLine(JsonSerializer.Serialize(result));
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }

        public static async Task DeleteUserById(GraphServiceClient graphClient)
        {
            Console.Write("Enter user object ID: ");
            string userId = Console.ReadLine();

            Console.WriteLine($"Looking for user with object ID '{userId}'...");

            try
            {
                // Delete user by object ID
                await graphClient.Users[userId]
                   .Request()
                   .DeleteAsync();

                Console.WriteLine($"User with object ID '{userId}' successfully deleted.");
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }

        public static async Task SetPasswordByUserId(GraphServiceClient graphClient)
        {
            Console.Write("Enter user object ID: ");
            string userId = Console.ReadLine();

            Console.Write("Enter new password: ");
            string password = Console.ReadLine();

            Console.WriteLine($"Looking for user with object ID '{userId}'...");

            var user = new User
            {
                PasswordPolicies = "DisablePasswordExpiration,DisableStrongPassword",
                PasswordProfile = new PasswordProfile
                {
                    ForceChangePasswordNextSignIn = false,
                    Password = password,
                }
            };

            try
            {
                // Update user by object ID
                await graphClient.Users[userId]
                   .Request()
                   .UpdateAsync(user);

                Console.WriteLine($"User with object ID '{userId}' successfully updated.");
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }

        public static async Task BulkCreate(AppSettings config, GraphServiceClient graphClient)
        {
            // Get the users to import
            string appDirectoryPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
            string dataFilePath = Path.Combine(appDirectoryPath, config.UsersFileName);

            // Verify and notify on file existence
            if (!System.IO.File.Exists(dataFilePath))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"File '{dataFilePath}' not found.");
                Console.ResetColor();
                Console.ReadLine();
                return;
            }

            Console.WriteLine("Starting bulk create operation...");

            // Read the data file and convert to object
            UsersModel users = UsersModel.Parse(System.IO.File.ReadAllText(dataFilePath));

            foreach (var user in users.Users)
            {
                user.SetB2CProfile(config.TenantId);

                try
                {
                    // Create the user account in the directory
                    User user1 = await graphClient.Users
                                    .Request()
                                    .AddAsync(user);

                    Console.WriteLine($"User '{user.DisplayName}' successfully created.");
                }
                catch (Exception ex)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine(ex.Message);
                    Console.ResetColor();
                }
            }
        }

        public static async Task CreateUserWithCustomAttribute(GraphServiceClient graphClient, string b2cExtensionAppClientId, string tenantId)
        {
            if (string.IsNullOrWhiteSpace(b2cExtensionAppClientId))
            {
                throw new ArgumentException("B2C Extension App ClientId (ApplicationId) is missing in the appsettings.json. Get it from the App Registrations blade in the Azure portal. The app registration has the name 'b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.'.", nameof(b2cExtensionAppClientId));
            }

            // Declare the names of the custom attributes
            const string customAttributeName1 = "FavouriteSeason";
            const string customAttributeName2 = "LovesPets";

            // Get the complete name of the custom attribute (Azure AD extension)
            Helpers.B2cCustomAttributeHelper helper = new Helpers.B2cCustomAttributeHelper(b2cExtensionAppClientId);
            string favouriteSeasonAttributeName = helper.GetCompleteAttributeName(customAttributeName1);
            string lovesPetsAttributeName = helper.GetCompleteAttributeName(customAttributeName2);

            Console.WriteLine($"Create a user with the custom attributes '{customAttributeName1}' (string) and '{customAttributeName2}' (boolean)");

            // Fill custom attributes
            IDictionary<string, object> extensionInstance = new Dictionary<string, object>();
            extensionInstance.Add(favouriteSeasonAttributeName, "summer");
            extensionInstance.Add(lovesPetsAttributeName, true);

            try
            {
                // Create user
                var result = await graphClient.Users
                .Request()
                .AddAsync(new User
                {
                    GivenName = "Casey",
                    Surname = "Jensen",
                    DisplayName = "Casey Jensen",
                    Identities = new List<ObjectIdentity>
                    {
                        new ObjectIdentity()
                        {
                            SignInType = "emailAddress",
                            Issuer = tenantId,
                            IssuerAssignedId = "casey.jensen@example.com"
                        }
                    },
                    PasswordProfile = new PasswordProfile()
                    {
                        Password = Helpers.PasswordHelper.GenerateNewPassword(4, 8, 4)
                    },
                    PasswordPolicies = "DisablePasswordExpiration",
                    AdditionalData = extensionInstance
                });

                string userId = result.Id;

                Console.WriteLine($"Created the new user. Now get the created user with object ID '{userId}'...");

                // Get created user by object ID
                result = await graphClient.Users[userId]
                    .Request()
                    .Select($"id,givenName,surName,displayName,identities,{favouriteSeasonAttributeName},{lovesPetsAttributeName}")
                    .GetAsync();

                if (result != null)
                {
                    Console.ForegroundColor = ConsoleColor.Blue;
                    Console.WriteLine($"DisplayName: {result.DisplayName}");
                    Console.WriteLine($"{customAttributeName1}: {result.AdditionalData[favouriteSeasonAttributeName].ToString()}");
                    Console.WriteLine($"{customAttributeName2}: {result.AdditionalData[lovesPetsAttributeName].ToString()}");
                    Console.WriteLine();
                    Console.ResetColor();
                    Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
                }
            }
            catch (ServiceException ex)
            {
                if (ex.StatusCode == System.Net.HttpStatusCode.BadRequest)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine($"Have you created the custom attributes '{customAttributeName1}' (string) and '{customAttributeName2}' (boolean) in your tenant?");
                    Console.WriteLine();
                    Console.WriteLine(ex.Message);
                    Console.ResetColor();
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
                Console.ResetColor();
            }
        }
    }
}

使用 Microsoft Graph SDK 发出 API 调用中介绍了如何在 Microsoft Graph 中读取和写入信息,使用 $select 控制返回的属性,提供自定义查询参数,以及使用 $filter$orderBy 查询参数。

另请参阅