使用 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 一文中的步骤,以便创建供管理应用程序使用的应用程序注册。

用户管理

注意

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

用户电话号码管理(beta 版本)

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

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

Enable phone sign-in

注意

在当前 beta 版本中,此 API 仅在以下情况下运行:在国家/地区代码和电话号码之间加入空格的形式存储了电话号码。 默认情况下,Azure AD B2C 服务目前不添加此空间。

自助服务密码重置电子邮件地址(beta 版本)

一种电子邮件地址,用户名登录帐户可以使用它来重置密码。 有关详细信息,请参阅 Azure AD 身份验证方法 API

软件 OATH 令牌身份验证方法(测试版)

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

标识提供者

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

用户流

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

用户流身份验证方法(beta 版本)

选择允许用户通过本地帐户进行注册的机制。 本地帐户是 Azure AD 对其进行标识断言的帐户。 有关详细信息,请参阅 b2cAuthenticationMethodsPolicy 资源类型

自定义策略

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

策略密钥

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

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

信任框架策略密钥集

信任框架策略密钥

应用程序

应用程序扩展属性

Azure AD B2C 提供一个目录,每个用户可在其中保存 100 个扩展值。 若要管理用户的扩展值,请在 Microsoft Graph 中使用以下用户 API

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

审核日志

条件性访问

如何以编程方式管理 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 查询参数。