アプリ開発ときどきアウトドア

主にJavaを使ったアプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

.NET Core 1. システムエンジニアリング Azure 実装技術

.NET Core: Azure AD B2Cユーザアカウント操作のサンプル

投稿日:

概要

  • 管理者が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)を使用する前提でサンプルを作成しています。
    ※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のテナントは既に取得している前提のサンプルとなります。

実行環境の準備

サンプルを実行するためには、次の手順でAzure AD B2Cテナントにアプリの情報を登録します。
(この手順は、クライアント資格情報フローを使う前提になっています。)

  1. Azureポータルにログイン
  2. アプリの新規登録
    [Azure AD B2C]リソースを選択し、[アプリの登録]メニューの[新規登録]をクリックする。
  3. 次の内容を入力し、[登録]をクリックする。
    • [名前]: “GraphApiTest” ※任意の名前を指定可
    • [サポートされているアカウントの種類]: [この組織ディレクトリのみに含まれるアカウント]
      (この例では、Google, Twitter等の外部や別テナントを使用する予定はなく、Azure AD B2Cローカルアカウントのみを使用する想定のため。)
    • [リダイレクトURI]: (指定不要)
      (クライアント資格情報フローでは、リダイレクトを使用しないため)

  4. クライアントID、テナントIDの確認
    登録したアプリの概要が表示されるので、クライアントID, テナントIDをどこかに控えておきます。
  5. アプリの認証に使用するシークレットの作成
    メニューの[証明書とシークレット]を選択し、[新しいクライアントシークレット]をクリックし、新しい任意のシークレットを作成します。
    • 証明書を使用した方がより安全ですが、検証のためより簡単なシークレットを使用しています。
    • シークレットの値は次回表示時に参照できなくなってしまうので、どこかに控えてください。

  6. ユーザアカウント操作のための権限付与
    1. [Azure Active Directory]リソースの[ロールと管理者]メニューを選択し、[ユーザー管理者]をクリックします。
    2. [割り当ての追加]をクリックし、追加したアプリGraphApiTestを選択後、[追加]します。
      (既定ではユーザアカウントしか表示されませんが、名称で検索できます。)
  7. B2C拡張アプリのクライアントIDの確認
    [Azure AD B2C]リソースの[アプリの登録]メニューを選択し、[すべてのアプリケーション]をクリックします。
    表示された”b2c-extensions-app”アプリのクライアントIDを、どこかに控えます。

以下に補足を記載します。

  • 権限の設定については、次の情報を参考にしています。
    Microsoft Graph を使用して Azure AD B2C を管理する
    当初、[Directory.ReadWrite.All]のアクセス許可が必要かと思ったのですが、ユーザー管理者ロールの付与で問題ないようです。
  • ユーザー管理者ロールを付与しない場合、個別に次のアクセス許可やロールが必要です。
    • Azure AD B2Cの登録アプリの[APIとアクセス許可]-[アプリケーションの許可]から、[Directory.ReadWrite.All](ディレクトリ全般の操作のため)、[User.ReadWrite.All](ユーザアカウントの削除のため)を追加を追加し、同意する。
    • [Azure Active Directory]リソースの[ロールと管理者]メニューから、[ヘルプデスク管理者]ロールに”GraphApiTest”を追加する。
  • アプリ登録の[認証]にあるパブリッククライアントの設定について
    • [認証]-[アプリケーションはパブリッククライアントとして扱います。]という設定があり、選択肢「はい」か「いいえ」があります。今回のクライアント資格情報フローではリダイレクトを使用しないので、どちらか選択する必要があります。
    • この項目は、Open ID Connect(OAuth2)の”Client Types”に対応するようで、RFC上では”public client”または”confidential client”の選択肢があります。
      RFC 6749: The OAuth 2.0 Authorization Framework
    • 今回の場合、資格情報をサーバ上に保持するWebサーバを想定しており、RFCの解釈では”confidential client”(パブリッククライアントではない)となります。そのため、前述の選択肢は既定値の「いいえ」となり、上記の手順には含めていません。

サンプルコード

ユーザアカウントに対する一連の操作を順番に実行するサンプルとなります。
完全なコードは下記で公開しています。

プロジェクト構成

Graph APIを使用するためのパッケージの参照を追加します。

参考:
マイクロソフトのサイトにあるサンプルではMicrosoft.Graph.Authパッケージを使用しています。これはpreview版かつ公式リリース予定(GA)がないのでプロダクト環境向けには使用できません。その代替策として、Microsoft.Graph.Authパッケージと同様に、MSAL.NET(Microsoft.Identity.Clientパッケージ)を使用して認証を実装します。

メイン

トークンの取得から始まり、ユーザアカウントの作成、更新、削除を行います。
基本的には、GraphServiceClientを使用してユーザアカウントに対する各種操作を行います。
Microsoft Graphに対する認証を行うための認証プロバイダMyAuthProviderを生成し、GraphServiceClientに引き渡しています。

// 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要求に対して認証情報を設定する必要があります。

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クライアントを使用して、ユーザアカウント一覧を取得します。

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のユーザアカウントを取得します。

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ポータルでの作成時と同様の属性が設定されるようユーザアカウントを作成します。
参考として、サインインユーザ名、サインインメールアドレス、拡張属性・カスタム属性も設定するようにしています。

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上でユーザアカウントを識別するためのキーとなるサインインユーザ名、サインインメールアドレス、プリンシパル名を変更することもできます。

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型)に格納します。カスタム属性のプロパティ名に関しては、こちらをご覧ください。

ユーザアカウントの更新(パスワード更新)

ユーザアカウントのパスワードを更新します。
※ユーザアカウントのパスワード変更には、前述の通り追加の権限が必要となります。

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();
}

ユーザアカウントの削除

ユーザアカウントを削除します。
※ユーザアカウントの削除には、前述の通り追加の権限が必要となります。

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()を実行します。

実行結果の例

参考ですが、実行結果は次のようになります。

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


(adsbygoogle = window.adsbygoogle || []).push({});


(adsbygoogle = window.adsbygoogle || []).push({});

-.NET Core, 1. システムエンジニアリング, Azure, 実装技術

執筆者:

関連記事

grid

Excelで月初日、第1週日、第2週日を算出する

プロジェクト管理で毎年、毎月のタスクを管理するために、この辺の操作を行うためのExcel関数を調べたので記載しておきます。 Excelの関数式 ここでは、基本的に5営業日単位で管理したいことと、最初の …

DOSバッチリファレンス

ちょっとした事でbatファイルを作成することが多い。 そのたびにネット検索するのが非効率なため、リファレンス化しようと思う。 そういう目的なので、自分がよく調べる項目に絞っている。 開発時の注意点 直 …

Javaによるzipファイルの安全な解凍方法

以前、業務アプリ(Java)でzipファイルの操作が必要となったため、Javaにおけるzip圧縮解凍について調査しました。また、zip4jを使った圧縮・解凍についても説明しました。 ここでは、もう少し …

mybatis-generatorプラグインの実装方法

mybatis-generatorを使うことで、各テーブルを操作するためのクラス群を容易に準備することができます。しかしながら、mybatis-generatorが提供する機能では、システム開発で求め …

パスワード情報の保管方式の比較

Webアプリの開発でパスワードを使ったユーザ認証を設計・実装する機会がよくある。 後輩への説明や勉強会ネタとして、この辺の話を纏めてみようと思う。 概要 オンラインバンキングやネットショッピングのサイ …