#ユーザー委任とアプリケーション認証
Azure AD v1 エンドポイントの認証方法は 2 つあります。
ユーザーに委任されたアクセス許可
ユーザーが明示的にサインインして、アプリケーションで必要なアクセス許可に同意をする方法。
アプリケーションのアクセス許可
バックグランドサービス用で、事前に管理者が必要なアクセス許可に同意をし、プログラム実行時には認証を聞かれない方法。アプリケーション認証といわれることもある。
ユーザー委任と Active Directory Authentication Library (ADAL)
今回は以下のアプリケーションを作ります。
- C# コンソールアプリケーション
- ユーザー委任シナリオ
- ADAL を利用
アプリケーションの登録
OAuth 2.0 を使うために、まず初めにアプリケーションを登録します。
1. Azure ポータル にアクセスして、ログイン。ここでは組織アカウントを使用。ログイン後、Azure Active Directory を選択。
2. 「アプリの登録」を選択し、「新しいアプリケーションの登録」をクリック。
3. 名前を指定し、「アプリケーションの種類」から「ネイティブ」を選択。任意の「リダイレクト URI」を指定し「作成」をクリック。
4. アプリが作成されたら「アプリション ID」を確認。その後「設定」をクリック。
5. 「必要なアクセス許可」をクリックし、「追加」をクリック。
6. 「API を選択します」をクリックして、「Microsoft Graph」をクリック。「選択」ボタンをクリック。
7. 「アクセス許可を選択します」をクリックし、必要な権限を追加。ここでは Have full access to user calendars を選択し、最後に「完了」をクリック。
アプリケーションの開発
1. Visual Studio で C# のコンソールアプリケーションプロジェクトを作成。尚、.NET Core のコンソールアプリケーションは UI を出せないため、今回のコードでは動作しません。UI 出せない場合の対応は別の記事で紹介します。
2. NuGet の管理で 「Microsoft.IdentityModel.Clients.ActiveDirectory (ADAL)」および「JSON.NET」を追加。
3. 参照の追加より System.Security アセンブリを追加。
4. TokenCacheHelper.cs ファイルを追加し、以下のコードと差し替え。これは ADAL で取得したトークンキャッシュをローカルディスクに保存するためのクラス。
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO;
using System.Security.Cryptography;
namespace GraphUserDemo
{
public partial class Program
{
class TokenCacheHelper
{
/// <summary>
/// Get the user token cache
/// </summary>
/// <returns></returns>
public static TokenCache GetTokenCache()
{
if (myTokenCache == null)
{
myTokenCache = new TokenCache();
myTokenCache.BeforeAccess = BeforeAccessNotification;
myTokenCache.AfterAccess = AfterAccessNotification;
}
return myTokenCache;
}
static TokenCache myTokenCache;
/// <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 System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;
namespace GraphUserDemo
{
public partial class Program
{
private static AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common/", TokenCacheHelper.GetTokenCache());
private static string clientId = "f0bf59c9-50d9-4af2-97e6-8d6c401a9c4d";
private static Uri redirectUri = new Uri("http://localhost/myapp");
private string graphUrl = "https://graph.microsoft.com";
static void Main(string[] args)
{
Program p = new Program();
p.Run();
Console.Read();
}
private async Task Run()
{
AuthenticationResult authResult = null;
try
{
if (authContext.TokenCache.ReadItems().Count() > 0)
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority, TokenCacheHelper.GetTokenCache());
authResult = await authContext.AcquireTokenSilentAsync(graphUrl, clientId);
}
catch (AdalSilentTokenAcquisitionException ex)
{
authResult = await authContext.AcquireTokenAsync(graphUrl, clientId, redirectUri, new PlatformParameters(PromptBehavior.Always));
}
catch (Exception ex)
{
}
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.BaseAddress = new Uri(graphUrl);
var result = await client.GetAsync("/v1.0/me");
if (result.IsSuccessStatusCode)
Console.WriteLine(JsonConvert.SerializeObject(
JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
Formatting.Indented));
}
}
}
}
6. F5 キーを押下してプログラムを実行。任意のユーザーでサインインして、権限に同意。登録した権限を全て要求されていることを確認。
7. 実行結果が取得できていることを確認。
8. アプリケーションを再起動しても、トークンがキャッシュされているため認証は聞かれないことを確認。
コードの詳細確認
AuthenticationContext
ADAL の基本クラスである AuthenticationContext に、認証で使う Authority の値と、トークンキャッシュを渡します。今回はキャッシュをローカルに保存できるよう独自クラスを実装してるため、そちらを利用。
認証/認可
AcquireTokenSilentAsync メソッドはユーザーサインインを求めずトークンの取得を試みます。キャッシュがある場合はこれだけでトークンが取得できます。失敗した場合、AdalSilentTokenAcquisitionException 例外が発生します。
await pca.AcquireTokenSilentAsync(scopes, pca.Users.FirstOrDefault());
AcquireTokenAsync はユーザーにサインインを求めてトークンを取得します。引数に new PlatformParameters(PromptBehavior.Always) を指定すると、常にサインイン画面を出せますが、他の引数も利用可能です。
authResult = await authContext.AcquireTokenAsync(graphUrl, clientId, redirectUri, new PlatformParameters(PromptBehavior.Always));
取得できるトークン
AuthenticationResult はアクセストークン以外に、ID トークンを含みます。これは認証情報です。
Microsoft Graph の呼び出し
HttpClient を使って Graph を呼び出しています。Graph の C# SDK はまた別の機会に紹介します。
コードを改修して予定を取得
ではこちらのサンプルを少し改修して、カレンダーの予定をとってみましょう。Run メソッド内の一部を書き換えます。
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.BaseAddress = new Uri(graphUrl);
var result = await client.GetAsync("/v1.0/me");
if (result.IsSuccessStatusCode)
Console.WriteLine(JsonConvert.SerializeObject(
JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
Formatting.Indented));
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 v1 認証を使う全てのアプリケーションで使えるため、是非一度試してください。
参照
Azure ポータル
Azure AD の認証シナリオ
Token cache serialization
Azure Active Directory のコード サンプル (V1 エンドポイント)