#ユーザー委任とアプリケーション認証
Azure AD v2 エンドポイントの認証方法は 2 つあります。
ユーザーに委任されたアクセス許可
ユーザーが明示的にサインインして、アプリケーションで必要なアクセス許可に同意をする方法。
アプリケーションのアクセス許可
バックグランドサービス用で、事前に管理者が必要なアクセス許可に同意をし、プログラム実行時には認証を聞かれない方法。アプリケーション認証といわれることもある。
ユーザー委任と Microsoft Authentication Library (MSAL)
今回は以下のアプリケーションを作ります。
- C# コンソールアプリケーション
- ユーザー委任シナリオ
- MSAL を利用
アプリケーションの登録
OAuth 2.0 を使うために、まず初めにアプリケーションを登録します。
1. Microsoft アプリ登録ポータル にアクセスして、ログイン。ここでは組織アカウントを使用。
2. 画面右上の「アプリの追加」をクリックし、アプリケーション名を入力後、Create をクリック。
3. 画面に表示されているアプリケーション ID をコピー。この ID がアプリケーションの固有識別子となる。
4. プラットフォームの項目で「プラットフォームの追加」をクリック。選択肢が出るので必要なものを選択。ここではネイティブアプリケーションを選択。
5. Microsoft Graph のアクセス許可の「委任されたアクセス許可」項目で、「追加」をクリック。アプリケーションのアクセス許可はアプリケーション認証用のため、次回見ていきます。
6. 必要な権限を追加。ここでは Calendars.ReadWrite を選択。
7. 最後に「保存」をクリック。
アプリケーションの開発
1. Visual Studio で C# のコンソールアプリケーションプロジェクトを作成。尚、.NET Core のコンソールアプリケーションは UI を出せないため、今回のコードでは動作しません。
2. NuGet の管理で 「Microsoft.Identity.Client (MSAL)」および「JSON.NET」を追加。尚 MSAL はプレビューのため、プレビューを含める。
3. 参照の追加より System.Security アセンブリを追加。
4. 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 など適宜書き換え。
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" は既定で入っている。
7. 実行結果が取得できていることを確認。
コードの詳細確認
PublicClientApplication
MSAL は 2 つのメインクラスがあります。PublicClientApplication はユーザー委任で利用するクラスです。
引数
- ClientId : 登録したアプリケーションの ID
- ログインテナント : 特定のテナントか、今回のように common にすればマルチテナント対応
- TokenCache : 認証済ユーザーをキャッシュするオブジェクト
委任のスコープ
アプリケーションにはカレンダーの権限も入れましたが、サインイン時に求められなかったのは、アプリケーション内でその権限を要求しなかったためです。尚、本来スコープは [リソース名]/[アクセス許可] のフォーマットで記述しますが、Microsoft Graph だけは例外で、リソース名を省略することが出来るため、アクセス許可だけを記述できます。
string[] scopes = new string[] { "user.read" };
認証/認可
AcquireTokenSilentAsync メソッドはユーザーサインインを求めずトークンの取得を試みます。キャッシュがある場合はこれだけでトークンが取得できます。失敗した場合、MsalUiRequiredException 例外が発生します。
await pca.AcquireTokenSilentAsync(scopes, pca.Users.FirstOrDefault());
AcquireTokenAsync はユーザーにサインインを求めてトークンを取得します。引数に UIBehavior.Consent を指定すると、同意画面も毎回表示できます。
await pca.AcquireTokenAsync(scopes, pca.Users.FirstOrDefault(), UIBehavior.Consent, null);
取得できるトークン
AuthenticationResult はアクセストークン以外に、ID トークンを含みます。これは認証情報です。
Microsoft Graph の呼び出し
HttpClient を使って Graph を呼び出しています。Graph の C# SDK はまた別の機会に紹介します。
コードを改修して予定を取得
ではこちらのサンプルを少し改修して、カレンダーの予定をとってみましょう。追加で Calender.Read を要求しています。Run メソッドを以下のコードを差し替えてください。
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));
}
}
予定が取得できています。
まとめ
ユーザー委任は OAuth 2.0 の基本シナリオです。Microsoft Graph だけではなく、Azure AD v2 認証を使う全てのアプリケーションで使えるため、是非一度試してください。
参照
Microsoft アプリ登録ポータル
C# WPF MSAL および Graph サンプル
Microsoft Authentication Library for .NET