概要
- 管理者がAzure AD B2Cのユーザアカウントの管理が行えるASP.NET Coreのアプリの開発を想定している。Azure AD B2Cユーザアカウントの作成や更新等の操作はMicrosoft Graphを使用する必要があり、その技術調査としてサンプルを作成しました。
- .NET CoreでMicrosoft Graphを操作するためのライブラリとして、Microsoft Graph API SDKが提供されているため、これを使用しています。(.NET Core用のMicrosoft.Graphパッケージを想定、Visul StudioのNuGetで利用可。)
- Microsoft Graphの認証はMicrosoft Identityプラットフォームをベースにしています。
前述の背景を踏まえ、今回はデーモンアプリのシナリオ(※1)を使用する前提でサンプルを作成しています。- Microsoft ID プラットフォームの概要
- シナリオ:Web API を呼び出すデーモン アプリケーション
- Microsoft ID プラットフォームと OAuth 2.0 クライアント資格情報フロー
※1: 使用する認証フローは「クライアント資格情報フロー」(Open ID Connect(OAuth2)の”client credentials flow”)となります。(認証対象はユーザではなくアプリケーション。) - 上記のクライアント資格情報フローを実装するために、Microsoft Identityプラットフォーム用ライブラリであるMicrosoft Authentication Library (MSAL)を使用します。
- ユーザアカウントの作成・更新では、検証のために拡張属性・カスタム属性の更新も含めています。
両者の違いについては、こちらをご覧ください。 - 今回は技術検証を目的としているため、ASP.NET Coreではなく、より実装が容易な.NET Core Console形態のプロジェクトを使用しています。(Microsoft Graph API SDKの使い方は両者で変わりません。)
- 2020年9月現在の最新であるMicrosoft Graph v1.0を使用します。
- Azure AD B2Cのテナントは既に取得している前提のサンプルとなります。
実行環境の準備
こちらで紹介しています。
目次 1 概要2 実行環境の準備3 補足事項 概要 WebアプリからGraphAPIを使用してAzure AD B2C上…
サンプルコード
ユーザアカウントに対する一連の操作を順番に実行するサンプルとなります。
完全なコードは、こちらで公開しています。
プロジェクト構成
Graph APIを使用するためのパッケージの参照を追加します。
- Microsoft.Graph(3.13.0)
- Microsoft.Identity.Client(4.18.0)
マイクロソフトのサイトにあるサンプルではMicrosoft.Graph.Authパッケージを使用しています。これはpreview版かつ公式リリース予定(GA)がないのでプロダクト環境向けには使用できません。その代替策として、Microsoft.Graph.Authパッケージと同様に、MSAL.NET(Microsoft.Identity.Clientパッケージ)を使用して認証を実装します。
メイン
トークンの取得から始まり、ユーザアカウントの作成、更新、削除を行います。
基本的には、GraphServiceClientを使用してユーザアカウントに対する各種操作を行います。
Microsoft Graphに対する認証を行うための認証プロバイダMyAuthProviderを生成し、GraphServiceClientに引き渡しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // Azure AD B2Cに登録したアプリのクライアントID const string ClientId = "11111111-2222-3333-4444-555555555555"; // Azure AD B2CテナントのID const string TenantId = "mytenant.onmicrosoft.com"; // 本来はKey Vault等の安全なストアを推奨 const string Secret = "xxxxxxxxxxxxxxxxxxx"; // カスタム属性操作のための"b2c-extensions-app"アプリのクライアントID const string B2CExtClientId = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"; // 拡張属性に含まれるカスタム属性名の定義 // 形式: "extension_{guid}_{属性名}" static string _exCusAttrPrefix = $"extension_{B2CExtClientId.Replace("-", "")}_"; static string _exCusAttrCustomString = _exCusAttrPrefix + "customString"; static string _exCusAttrCustomInt = _exCusAttrPrefix + "customInt"; static string _exCusAttrCustomBoolean = _exCusAttrPrefix + "customBoolean"; static async Task Main(string[] args) { // クライアントの認証に使用する認証プロバイダを生成 var authProvider = new MyAuthProvider(ClientId, TenantId, Secret); // Microsoft Graphを操作するためのクライアントの生成 var graphClient = new GraphServiceClient(authProvider); // ユーザアカウント一覧の取得 await GetUserList(graphClient); // ユーザアカウントの登録 var id = await CreateUser(graphClient); await GetUser(graphClient, id); // ユーザアカウントの更新 await UpdateUser(graphClient, id); await GetUser(graphClient, id); // ユーザアカウント(パスワード)の更新 await UpdateUserPassword(graphClient, id, "newpassword!"); // ユーザアカウントの削除 await DeleteUser(graphClient, id); } |
- 2, 4行目: 前章で確認したGraphApiTestアプリのクライアントID、テナントIDを指定します。
- 6行目: 前章で確認したシークレット値を指定します。
- 8行目: 前章で確認したB2C拡張アプリのクライアントIDを指定します。
- 11-14行目: 各所で使用する拡張属性の名称を定義しておきます。カスタム属性のプロパティ名に関しては、こちらをご覧ください。
認証プロバイダの実装
GraphServiceClientに対して認証機能を提供するクラス(認証プロバイダ)で、IAuthenticationProviderインターフェイスを実装する必要があります。
GraphServiceClientがMicrosoft Graphにアクセスする際、AuthenticateRequestAsync()が実行されます。このメソッドの中でアクセストークンの取得や、メソッド引数のHTTP要求に対して認証情報を設定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class MyAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private string[] _scopes; public MyAuthProvider(string clientId, string tenantId, string secret) { // Graph APIを使用する場合は固定 _scopes = new string[] { "https://graph.microsoft.com/.default" }; // Client Credentialsフローの場合は、機密クライアントアプリケーションを使用 _msalClient = ConfidentialClientApplicationBuilder .Create(clientId) .WithTenantId(tenantId) .WithClientSecret(secret) .Build(); } public async Task<string> GetAccessToken() { // TODO: 要件に応じてトークン取得のリトライ、キャッシングを実装 var result = await _msalClient.AcquireTokenForClient(_scopes).ExecuteAsync(); return result.AccessToken; } public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await GetAccessToken()); } } |
- 1行目: IAuthenticationProviderインターフェイスを実装します。
- 13-17, 23行目: 前述のようにクライアント資格情報フローを想定しており、アクセストークンの取得方法はMicrosoft Identity プラットフォームの資料を参考にしています。
- 23行目: プロダクト環境での使用を想定する場合、要件に応じてトークン取得のリトライやトークンのキャッシング等の実装を推奨します。一部はMicrosoft.Graph.AuthのClientCredentialProviderの実装が参考になると思います。
- 29-30行目: Microsoft Graphの認証仕様に合わせて、AuthorizationヘッダにBearer形式のアクセストークンを設定します。
ユーザアカウント一覧の取得
GraphServiceClientクライアントを使用して、ユーザアカウント一覧を取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | static async Task GetUserList(GraphServiceClient client) { Console.WriteLine("getting user list..."); var resultList = new List<User>(); // 最初のページ分のユーザ一覧を取得 var usersPage = await client.Users .Request() .Select(e => new { e.Id, e.DisplayName }) .OrderBy("displayName") .GetAsync(); resultList.AddRange(usersPage.CurrentPage); // 次のページ以降のユーザ一覧を取得 while(usersPage.NextPageRequest != null) { usersPage = await usersPage.NextPageRequest.GetAsync(); resultList.AddRange(usersPage.CurrentPage); } Console.WriteLine($"User Count: {resultList.Count}"); foreach (var u in resultList) { Console.WriteLine($"ObjectId={u.Id}, displayName={u.DisplayName}"); } Console.WriteLine(); } |
- 8-16行目: Usersリソースを指定してGetAsync()を実行することで、ユーザアカウント一覧を取得できます。
Select()で抽出する属性を定義しています。”ObjectId, DisplayName”等のように文字列でも指定できますが、間違える可能性があるので、この例のようにプロパティ名を指定することをお薦めします。ただし、後述のようにカスタム属性を指定する場合、文字列で指定する必要があります。
(なお、名前を間違えてもエラーは発生せず値が取得できないだけです。) - 20-24行目: ユーザアカウントが多数ある場合、GetAsync()の実行では先頭の100件しか取得できません。後続のデータの有無の確認やその取得のために、NextPageRequestプロパティを使用しています。
ユーザアカウントの取得
指定したObjectIdのユーザアカウントを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | static async Task GetUser(GraphServiceClient client, string id) { Console.WriteLine("getting user..."); // カスタム属性に値を指定する場合はUser.AdditionalDataを使用するが // 値を取得する場合は、User.AdditionalDataでは取得不可 // 回避策として、次の2つの方法が考えられるが、 // わざわざ要求を再送信するのも手間なので(1)の方法を使用する。 // (1) 文字列でカスタム属性の名称を指定(結果として他の属性目も文字列指定) // (2) User.Extensionsを別途取得する。 // (client.Users[id].Extensions.Request().GetAsync()) var result = await client.Users[id] .Request() .Select( nameof(User.Id) + "," + nameof(User.DisplayName) + "," + nameof(User.Identities) + "," + nameof(User.UserPrincipalName) + "," + nameof(User.MailNickname) + "," + nameof(User.AccountEnabled) + "," + nameof(User.PasswordProfile) + "," + nameof(User.PasswordPolicies) + "," + nameof(User.OtherMails) + "," + nameof(User.EmployeeId) + "," + _exCusAttrCustomString + "," + _exCusAttrCustomInt + "," + _exCusAttrCustomBoolean ) .GetAsync(); ShowUser(result); } |
- 13-30行目: Users[ObjectId]で対象のユーザアカウントを指定して、GetAsync()を実行します。
カスタム属性の値を取得する場合、現状のSDK APIの仕様では文字列で属性名を指定する必要があります。
ユーザアカウントの作成
Azureポータルでの作成時と同様の属性が設定されるようユーザアカウントを作成します。
参考として、サインインユーザ名、サインインメールアドレス、拡張属性・カスタム属性も設定するようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | static async Task<string> CreateUser(GraphServiceClient client) { Console.WriteLine("creating user..."); // 基本情報 var newGuid = Guid.NewGuid().ToString(); var user = new User() { DisplayName = "テストユーザ1", // サインインユーザ名、サインインメールアドレス // (ポータルだと1つ以上必須) Identities = new List<ObjectIdentity>() { new ObjectIdentity() { SignInType = "userName", Issuer = TenantId, IssuerAssignedId = "testuser01" }, new ObjectIdentity() { SignInType = "emailAddress", Issuer = TenantId, IssuerAssignedId = "testuser01@example.com" } }, // ポータルと同様にGUIDを設定 UserPrincipalName = $"{newGuid}@{TenantId}", // ポータルと同様にGUIDを設定 MailNickname = newGuid, // ポータルの[サインインのブロック]に対応(意味が逆) AccountEnabled = true, // ポータルと同様に、次回サインイン時のパスワード変更を要求しない PasswordProfile = new PasswordProfile() { Password = "password", ForceChangePasswordNextSignIn = false }, // ポータル同様に、無期限、複雑性要求なしを指定 PasswordPolicies = "DisablePasswordExpiration, " + "DisableStrongPassword", }; // 拡張属性に含まれる既定の属性・カスタム属性の指定サンプル // 拡張属性に含まれる従業員IDはプロパティ指定可 user.EmployeeId = "A12346"; // 拡張属性に含まれるカスタム属性はAdditionalDataで指定 user.AdditionalData = new Dictionary<string, object>() { //["employeeId"] = "A12346", [_exCusAttrCustomString] = "ハロー", [_exCusAttrCustomInt] = 123456, [_exCusAttrCustomBoolean] = true }; // ユーザアカウントの作成 var result = await client.Users .Request() .AddAsync(user); Console.WriteLine($"created: Id={result.Id}"); Console.WriteLine(); return result.Id; } |
- 60-62: Userオブジェクトのプロパティに属性値を指定し、Usersに対してPostAsync(User)を実行します。
ユーザアカウント作成時の必須項目はDisplayName, UserPrincipalName, MailNickname, AccountEnabled, PasswordProfile.Passwordですが、これはAzureポータルで作成した場合と異なります。 - 51行目: 拡張属性の値はUser.AdditionalDataプロパティ(Dictionary型)に格納します。カスタム属性のプロパティ名に関しては、こちらをご覧ください。
ユーザアカウントの更新
指定したObjectIdのユーザアカウントを更新します。
Azure AD B2C上でユーザアカウントを識別するためのキーとなるサインインユーザ名、サインインメールアドレス、プリンシパル名を変更することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | static async Task UpdateUser(GraphServiceClient client, string id) { Console.WriteLine("updating user..."); // 基本情報 var user = new User() { DisplayName = "テストユーザ1更新", // ユーザ特定のキーとなるサインインユーザ名とメールアドレスを変更 Identities = new List<ObjectIdentity>() { new ObjectIdentity() { SignInType = "userName", Issuer = TenantId, IssuerAssignedId = "testuser01update" }, new ObjectIdentity() { SignInType = "emailAddress", Issuer = TenantId, IssuerAssignedId = "testuser01update@example.com" } }, // ユーザ特定のキーとなるUPNを変更 UserPrincipalName = $"test1234567890@{TenantId}", // サインインのブロック AccountEnabled = false, // 連絡用メールアドレスの変更サンプル OtherMails = new String[]{ "contact1@example.com", "contact2@example.com" } }; // 拡張属性のカスタム属性の変更サンプル user.AdditionalData = new Dictionary<string, object>() { [_exCusAttrCustomString] = "ハローupdate", [_exCusAttrCustomInt] = 654321, [_exCusAttrCustomBoolean] = false }; // ユーザアカウントの更新 var result = await client.Users[id] .Request() .UpdateAsync(user); Console.WriteLine($"updated: Id={id}"); Console.WriteLine(); } |
- 44-46: Userオブジェクトに変更したい属性値を格納します。Users[ObjectId]で更新対象のユーザアカウントを指定し、UpdateAsync(User)を実行します。
- 36行目: 拡張属性の値はUser.AdditionalDataプロパティ(Dictionary型)に格納します。カスタム属性のプロパティ名に関しては、こちらをご覧ください。
ユーザアカウントの更新(パスワード更新)
ユーザアカウントのパスワードを更新します。
※ユーザアカウントのパスワード変更には、前述の通り追加の権限が必要となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | static async Task UpdateUserPassword( GraphServiceClient client, string id, string password) { Console.WriteLine("updating user password..."); var user = new User() { PasswordProfile = new PasswordProfile() { Password = password, ForceChangePasswordNextSignIn = false } }; await client.Users[id] .Request() .UpdateAsync(user); Console.WriteLine($"updated: Id={id}"); Console.WriteLine(); } |
ユーザアカウントの削除
ユーザアカウントを削除します。
※ユーザアカウントの削除には、前述の通り追加の権限が必要となります。
1 2 3 4 5 6 7 8 9 | static async Task DeleteUser(GraphServiceClient client, string id) { Console.WriteLine("deleting user..."); await client.Users[id] .Request() .DeleteAsync(); Console.WriteLine($"deleted: Id={id}"); Console.WriteLine(); } |
- 4-6行目: Users[ObjectId]で対象のユーザアカウントを指定して、DeleteAsync()を実行します。
実行結果の例
参考ですが、実行結果は次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | getting user list... User Count: 2 ObjectId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, displayName=佐藤 次郎 ObjectId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, displayName=鈴木 一郎 creating user... created: Id=70796d64-866b-4ed2-aae8-11286689b04d getting user... > Id : 70796d64-866b-4ed2-aae8-11286689b04d > DisplayName : テストユーザ1 > Identities.emailAddress : testuser01@example.com > Identities.userName : testuser01 > Identities.userPrincipalName : 7df87eac-e2ef-4e15-9b70-75ae0ca1f5e2@mytenant.onmicrosoft.com > UserPrincipalName : 7df87eac-e2ef-4e15-9b70-75ae0ca1f5e2@mytenant.onmicrosoft.com > MailNickname : 7df87eac-e2ef-4e15-9b70-75ae0ca1f5e2 > AccountEnabled : True > PasswordPolicies : DisablePasswordExpiration, DisableStrongPassword > EmployeeId : A12346 > AdditionalData.extension_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_customString: ハロー > AdditionalData.extension_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_customInt: 123456 > AdditionalData.extension_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_customBoolean: True updating user... updated: Id=70796d64-866b-4ed2-aae8-11286689b04d getting user... > Id : 70796d64-866b-4ed2-aae8-11286689b04d > DisplayName : テストユーザ1更新 > Identities.emailAddress : testuser01update@example.com > Identities.userName : testuser01update > Identities.userPrincipalName : test1234567890@mytenant.onmicrosoft.com > UserPrincipalName : test1234567890@mytenant.onmicrosoft.com > MailNickname : 7df87eac-e2ef-4e15-9b70-75ae0ca1f5e2 > AccountEnabled : False > PasswordPolicies : DisablePasswordExpiration, DisableStrongPassword > OtherMails[0] : contact2@example.com > OtherMails[1] : contact1@example.com > EmployeeId : A12346 > AdditionalData.extension_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_customString: ハローupdate > AdditionalData.extension_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_customInt: 654321 > AdditionalData.extension_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_customBoolean: False updating user password... updated: Id=70796d64-866b-4ed2-aae8-11286689b04d deleting user... deleted: Id=70796d64-866b-4ed2-aae8-11286689b04d |