uniface.hub

  • 2021年2月17日
  • 小西拓実
C# から Microsoft Graph API を利用して Azure AD B2C にユーザーを登録するまで

この記事について

IB-Mesの利用者登録周りでMicrosoft Graph APIに触る機会があったので、ざっくりとした使い方やハマったところなどを備忘録もかねて書いておきます。

Microsoft Graph とは

マイクロソフトによると、

「Microsoft Graph は、Microsoft 365 のデータとインテリジェンスへの入り口です。」

とのことなので、おそらくMicrosoft 365みたいなサービスに対する共通のインターフェースを提供してくれるもののようです(たぶん(きっと。
ということで今回はこいつを使ってAzure AD B2Cへのアクセスとユーザーの登録までをやってみます。

準備

Microsoft Graph のインストール

必要なパッケージをNuGetでインストールします。

インストールするパッケージは以下になります。

Microsoft.Identity.ClientはAzure ADへのアクセス時に使用するトークンの取得などをやってくれるライブラリ(MSAL: Microsoft Authentication Library)です。

コード

ではコードを見ていきます。
流れとしては、

  • GraphServiceClientの作成
  • ユーザーの登録

というシンプルなものになります。

GraphServiceClientの作成

using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;

namespace HelloGraph
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            var tenantId = "{my-tenant-id}";
            var clientId = "{my-app-client-id}";
            var clientSecret = "{my-app-client-secret}";

            var client = ConfidentialClientApplicationBuilder
                .Create(clientId)
                .WithTenantId(tenantId)
                .WithClientSecret(clientSecret)
                .Build();

            var authProvider = new ClientCredentialProvider(client);

            var graphClient = new GraphServiceClient(authProvider);

            ...
        }
    }
}

まずConfidentialClientApplicationBuilder.Build()を利用してConfidentialClientApplicationのインスタンスを作成します。
このときAzureにおけるアプリの、テナントID・クライアントID・クライアントシークレットを指定します。
※これらの値は秘匿の情報になるため、実際のコードに含めずセキュアな方法で管理することをおすすめします。Azure Key Vaultはいいものだ)

作成したConfidentialClientApplicationを基にClientCredentialProviderを作成し、これをGraphServiceClientのコンストラクタに渡して初期化します。簡単ですね。

ユーザーの登録

using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;

namespace HelloGraph
{
    class Program
    {
        public static Task Main(string[] args)
        {
            ...

            var userName = "ユニフェイス太郎";
            var userPassword = "PASUWADO1234";
            var userEmail = "taro_uniface@outlook.com";
            var tenant = "{my-tenant}.onmicrosoft.com";

            // 登録ユーザー作成
            var userToAdd = new Microsoft.Graph.User
            {
                AccountEnabled = true,
                DisplayName = userName,
                PasswordProfile = new PasswordProfile
                {
                    Password = userPassword,
                    ForceChangePasswordNextSignIn = false,
                    ForceChangePasswordNextSignInWithMfa = false,
                },
                Mail = userEmail,
                MailNickname = Guid.NewGuid().ToString(),
                Identities = new []
                {
                    new ObjectIdentity
                    {
                        Issuer = tenant,
                        IssuerAssignedId = userEmail,
                        SignInType = "emailAddress"
                    },
                },
                UserPrincipalName = $"{mailNickname}@{tenant}",
            };

            // 登録
            var user = await graphClient.Users.Request().AddAsync(userToAdd);

            ...
        }
    }
}

パッと見ややこいですが、Userオブジェクトを作成してGraphServiceClient.Users.Request().AddAsync()に渡しているだけです。
ユーザーの登録自体はこれだけで完了なのですが、Userオブジェクト作成時のパラメータが複雑なので少し説明します。

Microsoft.Graph.User オブジェクト

Microsoft.Graph.Userオブジェクトは単なるDTOです。
なのでほとんどのプロパティにはセッターが用意されているのですが、一部のプロパティは読み取り専用となっているので注意が必要です。
読み取り専用のプロパティに書き込もうした場合、コンパイルは通りますが実行時にエラーになります。
どのプロパティが読み取り専用かはプロパティのコメントにRead-onlyの記載があります。

読み取り専用プロパティ

また、一部のプロパティは必須となっており、設定しなかった場合にも実行時(AddAsync()によるPOST時)にエラーが発生します。
以下のプロパティは設定が必須になっています(他にもあるかも)。

  • AccountEnabled
  • PasswordProfile
  • MailNickname
  • UserPrincipalName

また、プロパティの中には正しく設定しないとエラーになったりサインインできなくなるものがあるため、ハマりやすい(実際にハマった)ものについて説明します。

MailNickname プロパティ

ユーザー登録のコードではMailNicknameに新規のGUIDを設定していますが、これはAzure Portalからユーザーを登録した際に適当なGUIDが振られるためその挙動に合わせています。
プロパティの名前に従うのであればメールアドレスの一部を設定するのが良いかもしれません。
また一部の記号は使用できないなどの制約があります。

Identities プロパティ

Identitiesプロパティに設定するObjectIdentityオブジェクトはサインイン時に要求する情報に応じて正しく初期化する必要があります。
このプロパティの内容に問題があると作成したユーザーでサインインできないなどの問題が発生するので注意が必要です。
このプロパティは省略可能ですが、省略した場合はUserPrincipalNameに設定した文字列でサインインすることができます。

注意点として、読み違えてなければObjectIdentity.IssuerAssignedIdプロパティのコメントには「SignInType"mailAddress"の時はメールアドレスのローカル部(@より前)を設定しろ」みたいなことが書いてありますが@以降も含めて設定しないとエラーになります

メールアドレスによるサインインとユーザー名によるサインインのObjectIdentityの初期化コードを載せておきます。

・メールアドレスでサインインする場合

new ObjectIdentity
{
    Issuer = tenant,
    IssuerAssignedId = "taro_uniface@example.com",
    SignInType = "emailAddress"
}

・ユーザー名でサインインする場合

new ObjectIdentity
{
    Issuer = tenant,
    IssuerAssignedId = "taro uniface",
    SignInType = "userName"
}

UserPrincipalName プロパティ

UserPrincipalNameプロパティですが、Azure AD B2Cでは***@{tenant}.onmicrosoft.comの形式で設定する必要があります。
{tenant}の部分はテナントIDではなくテナント名になるので注意。

余談ですが、先のIdentitiesを省略するとUserPrincipalNameが使用されるため、当初ここにユーザーのメールアドレスを指定すればよいのでは?と思ったのですがダメでした(全くサインインできずに悩みました)。
なので、少なくともAzure AD B2Cを利用する場合にはIdentitiesプロパティを正しく設定しないとメールアドレスによるサインインが行えないことになります。

ちなみにローカルのADからユーザーを連携した場合はUserPrincipalNameには{user-email}#EXT#@{tenant}.onmicrosoft.comのような文字列が設定されます。

おまけ

カスタム属性の付与

おまけとしてカスタム属性の設定についても書いておきます。

var user = new Microsoft.Graph.User
{
    AdditionalData = new Dictionary<string, object>
    {
        { GetCompleteKey("my-attribute-1"), "foobar" },
        { GetCompleteKey("my-attribute-2"), 123 }
        ...
    },
    ...
};

// キーの作成
string GetCompleteKey(string key)
{
    var extAppClientId = "{ext-app-client-id}";

    return $"extension_{extAppClientId.Replace("-", string.Empty)}_{key}";
}

AdditionalDataプロパティにDictionaryを渡してやることでユーザーに任意の属性を設定することができます。

Azure AD B2Cに登録する場合にはキーの形式に決まりがあるため、上記のコードではGetCompleteKey()メソッドで正しい形式のキーを作成しています。その部分について少し説明します。

キーの作成

まず、extAppClientIdですが、Azure AD B2Cに登録されたb2c-extensions-appアプリのクライアントIDになります。
b2c-extensions-appアプリはAzure上でAD B2Cのリソースを作成すると自動で作成されます。

b2c-extensions-app

Azure AD B2Cに登録する際のカスタム属性のキーには、このクライアントIDを含める必要があります。

実際にプロパティに設定する際のキーはextension_{ext-app-client-id}_{attribute-key}といった形式になります。
クライアントIDはGUIDですが、キーに-(ハイフン)を含めることはできないのでキーとして含める際に-(ハイフン)を取り除く必要があります。

まとめ

今回はMicrosoft Graph APIを利用してAzure AD B2Cにユーザーを登録するまでを書いてみました。
細かいところでハマりどころがありますが、Graph API自体は結構簡単に操作できる印象でした。

正直、ガシガシGraph APIを使ってコードを書いていくことは少ないかと思いますが、今後誰かが(自分が)触る時に同じ罠にハマらないための轍として役立ててもらえたらと思います。