Help us understand the problem. What is going on with this article?

Microsoft Graph を使ってみよう : Azure AD v2 エンドポイントのユーザー委任を MSAL で行う

More than 1 year has passed since last update.

ユーザー委任とアプリケーション認証

Azure AD v2 エンドポイントの認証方法は 2 つあります。

ユーザーに委任されたアクセス許可
ユーザーが明示的にサインインして、アプリケーションで必要なアクセス許可に同意をする方法。

アプリケーションのアクセス許可
バックグランドサービス用で、事前に管理者が必要なアクセス許可に同意をし、プログラム実行時には認証を聞かれない方法。アプリケーション認証といわれることもある。

ユーザー委任と Microsoft Authentication Library (MSAL)

今回は以下のアプリケーションを作ります。

  • C# コンソールアプリケーション
  • ユーザー委任シナリオ
  • MSAL を利用

アプリケーションの登録

OAuth 2.0 を使うために、まず初めにアプリケーションを登録します。

1. Microsoft アプリ登録ポータル にアクセスして、ログイン。ここでは組織アカウントを使用。

Capture.PNG

2. 画面右上の「アプリの追加」をクリックし、アプリケーション名を入力後、Create をクリック。

Capture.PNG

3. 画面に表示されているアプリケーション ID をコピー。この ID がアプリケーションの固有識別子となる。
Capture.PNG

4. プラットフォームの項目で「プラットフォームの追加」をクリック。選択肢が出るので必要なものを選択。ここではネイティブアプリケーションを選択。

Capture.PNG

5. Microsoft Graph のアクセス許可の「委任されたアクセス許可」項目で、「追加」をクリック。アプリケーションのアクセス許可はアプリケーション認証用のため、次回見ていきます。

Capture.PNG

6. 必要な権限を追加。ここでは Calendars.ReadWrite を選択。

Capture.PNG

7. 最後に「保存」をクリック。

アプリケーションの開発

1. Visual Studio で C# のコンソールアプリケーションプロジェクトを作成。尚、.NET Core のコンソールアプリケーションは UI を出せないため、今回のコードでは動作しません。

image.png

2. NuGet の管理で 「Microsoft.Identity.Client (MSAL)」および「JSON.NET」を追加。尚 MSAL はプレビューのため、プレビューを含める。

image.png

3. 参照の追加より System.Security アセンブリを追加。

image.png

4. TokenCacheHelper.cs ファイルを追加し、以下のコードと差し替え。

TokenCacheHelper.cs
using Microsoft.Identity.Client;
using System.IO;
using System.Security.Cryptography;

namespace GraphUserDemo
{
    public partial class Program
    {
        static class TokenCacheHelper
        {
            /// <summary>
            /// Get the user token cache
            /// </summary>
            /// <returns></returns>
            public static TokenCache GetUserCache()
            {
                if (usertokenCache == null)
                {
                    usertokenCache = new TokenCache();
                    usertokenCache.SetBeforeAccess(BeforeAccessNotification);
                    usertokenCache.SetAfterAccess(AfterAccessNotification);
                }
                return usertokenCache;
            }

            static TokenCache usertokenCache;

            /// <summary>
            /// Path to the token cache
            /// </summary>
            public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin";

            private static readonly object FileLock = new object();

            public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
            {
                lock (FileLock)
                {
                    args.TokenCache.Deserialize(File.Exists(CacheFilePath)
                        ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                                    null,
                                                    DataProtectionScope.CurrentUser)
                        : null);
                }
            }

            public static void AfterAccessNotification(TokenCacheNotificationArgs args)
            {
                // if the access operation resulted in a cache update
                if (args.TokenCache.HasStateChanged)
                {
                    lock (FileLock)
                    {
                        // reflect changesgs in the persistent store
                        File.WriteAllBytes(CacheFilePath,
                                            ProtectedData.Protect(args.TokenCache.Serialize(),
                                                                    null,
                                                                    DataProtectionScope.CurrentUser)
                                            );
                        // once the write operationtakes place restore the HasStateChanged bit to filse
                        args.TokenCache.HasStateChanged = false;
                    }
                }
            }
        }
    }
}

5. Program.cs を以下のコードと差し替え。ClientID など適宜書き換え。

Program.cs
using Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using Newtonsoft.Json;

namespace GraphUserDemo
{
    public partial class Program
    {
        private static string clientId = "550dcc01-129d-4b51-b80c-96995a6848e3";
        private static string redirectUri = "http://localhost";
        private static PublicClientApplication pca;
        private static Uri graphUrl = new Uri("https://graph.microsoft.com");
        static void Main(string[] args)
        {
            #region GraphClient and Auth

            pca = new PublicClientApplication(
                clientId,
                "https://login.microsoftonline.com/common",
                TokenCacheHelper.GetUserCache());

            #endregion

            Program p = new Program();
            p.Run().Wait();
            Console.ReadLine();
        }

        private async Task Run()
        {
            using (HttpClient client = new HttpClient())
            {
                string[] scopes = new string[] { "user.read" };
                client.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
                client.BaseAddress = graphUrl;

                var result = await client.GetAsync("/v1.0/me");
                if (result.IsSuccessStatusCode)
                    Console.WriteLine(JsonConvert.SerializeObject(
                        JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()), 
                        Formatting.Indented));
            }
        }

        private async Task<string> GetAccessTokenAsync(string[] scopes)
        {
            AuthenticationResult authResult = null;

            try
            {
                authResult = await pca.AcquireTokenSilentAsync(scopes,
                    pca.Users.FirstOrDefault());
            }
            catch (MsalUiRequiredException ex)
            {
                authResult = await pca.AcquireTokenAsync(scopes);
            }

            if (authResult != null)
                return authResult.AccessToken;
            else
                return null;
        }
    }
}

6. F5 キーを押下してプログラムを実行。Read.User に該当する "Sign you in and read your profile" を聞かれることを確認。"Access your data anytime" は既定で入っている。

image.png

7. 実行結果が取得できていることを確認。

image.png

コードの詳細確認

PublicClientApplication
MSAL は 2 つのメインクラスがあります。PublicClientApplication はユーザー委任で利用するクラスです。

引数

 - ClientId : 登録したアプリケーションの ID
 - ログインテナント : 特定のテナントか、今回のように common にすればマルチテナント対応
 - TokenCache : 認証済ユーザーをキャッシュするオブジェクト

委任のスコープ
アプリケーションにはカレンダーの権限も入れましたが、サインイン時に求められなかったのは、アプリケーション内でその権限を要求しなかったためです。尚、本来スコープは [リソース名]/[アクセス許可] のフォーマットで記述しますが、Microsoft Graph だけは例外で、リソース名を省略することが出来るため、アクセス許可だけを記述できます。

Program.cs
string[] scopes = new string[] { "user.read" };

認証/認可
AcquireTokenSilentAsync メソッドはユーザーサインインを求めずトークンの取得を試みます。キャッシュがある場合はこれだけでトークンが取得できます。失敗した場合、MsalUiRequiredException 例外が発生します。

Program.cs
await pca.AcquireTokenSilentAsync(scopes, pca.Users.FirstOrDefault());

AcquireTokenAsync はユーザーにサインインを求めてトークンを取得します。引数に UIBehavior.Consent を指定すると、同意画面も毎回表示できます。

Program.cs
await pca.AcquireTokenAsync(scopes, pca.Users.FirstOrDefault(), UIBehavior.Consent, null);

取得できるトークン
AuthenticationResult はアクセストークン以外に、ID トークンを含みます。これは認証情報です。

image.png

Microsoft Graph の呼び出し
HttpClient を使って Graph を呼び出しています。Graph の C# SDK はまた別の機会に紹介します。

コードを改修して予定を取得

ではこちらのサンプルを少し改修して、カレンダーの予定をとってみましょう。追加で Calender.Read を要求しています。Run メソッドを以下のコードを差し替えてください。

Program.cs
private async Task Run()
{
    using (HttpClient client = new HttpClient())
    {
        string[] scopes = new string[] { "user.read" };
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
        client.BaseAddress = graphUrl;

        var result = await client.GetAsync("/v1.0/me");
        if (result.IsSuccessStatusCode)
            Console.WriteLine(JsonConvert.SerializeObject(
                JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()), 
                Formatting.Indented));

        scopes = new string[] { "calendars.read" };
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));

        result = await client.GetAsync("/v1.0/me/events");
        if (result.IsSuccessStatusCode)
            Console.WriteLine(JsonConvert.SerializeObject(
                JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
                Formatting.Indented));
    }
}

image.png

予定が取得できています。

image.png

まとめ

ユーザー委任は OAuth 2.0 の基本シナリオです。Microsoft Graph だけではなく、Azure AD v2 認証を使う全てのアプリケーションで使えるため、是非一度試してください。

目次へ戻る

参照

Microsoft アプリ登録ポータル
C# WPF MSAL および Graph サンプル
Microsoft Authentication Library for .NET

microsoft
マイクロソフトのメンバーが最新の技術情報をお届けします。Twitterアカウント(@msdevjp)やYouTubeチャンネル「クラウドデベロッパーちゃんねる」も運用中です。
https://aka.ms/MSFT-Docs-JPN
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away