#ユーザー委任とアプリケーション認証
Azure AD v2 エンドポイントの認証方法は 2 つあります。
ユーザーに委任されたアクセス許可
ユーザーが明示的にサインインして、アプリケーションで必要なアクセス許可に同意をする方法。
アプリケーションのアクセス許可
バックグランドサービス用で、事前に管理者が必要なアクセス許可に同意をし、プログラム実行時には認証を聞かれない方法。アプリケーション認証といわれることもある。
アプリケーション認証と Microsoft Authentication Library (MSAL)
今回は以下のアプリケーションを作ります。
- C# コンソールアプリケーション
- アプリケーション認証シナリオ
- MSAL を利用
アプリケーションの登録
OAuth 2.0 を使うために、まず初めにアプリケーションを登録します。
1. Microsoft アプリ登録ポータル にアクセスして、ログイン。ここでは組織アカウントを使用。
2. ユーザー委任シナリオで作成したアプリケーションを利用、または新規に登録。プラットフォームの項目で「プラットフォームの追加」をクリック。Web アプリケーションを選択。
3. アプリケーションシークレットの項目で「新しいパスワードを生成」をクリック。パスワードが表示されるのでコピーして保存。
※このパスワードは画面を消すと二度と取得できない。
4. Microsoft Graph のアクセス許可の「委任されたアクセス許可」項目で、「追加」をクリック。
5. 必要な権限を追加。ここでは User.ReadWrite.All を選択。
6. 最後に「保存」をクリック。
管理者の同意を取得
ユーザーなしでアクセスを行う場合、および権限に管理者の同意が必要な場合は、エンドポイントにリクエストを投げて同意を行います。
1. ブラウザに以下のアドレスを入力。
- テナント名は自分のものに変更
- client_id : 登録したアプリケーションのアプリケーション ID
- redirect_uri : 送信したリクエストの戻り先 (今回は受け取って処理しないので仮のアドレス)
- state : リクエストの応答に含まれる値で戻り先で検証が可能
https://login.microsoftonline.com/graphdemo01.onmicrosoft.com/adminconsent
?client_id=550dcc01-129d-4b51-b80c-96995a6848e3
&state=12345
&redirect_uri=http://localhost
2. サインイン画面が出るので、管理者権限でサインイン。この場合、アプリケーション権限だけでなく、ユーザーの権限も含めたすべての権限が表示される。よってカレンダーの権限も求められる。
3. 結果はリダイレクトされるので URL で admin_consent の値が True であることを確認。先ほど指定した redirect_uri にリダイレクトされ、また state も指定した元同じ値が戻る。
コンソールアプリケーションの開発
1. Visual Studio で C# のコンソールアプリケーションプロジェクトを作成。
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 GraphAppDemo
{
public partial class Program
{
static class TokenCacheHelper
{
/// <summary>
/// Get the user token cache
/// </summary>
/// <returns></returns>
public static TokenCache GetAppCache()
{
if (appTokenCache == null)
{
appTokenCache = new TokenCache();
appTokenCache.SetBeforeAccess(BeforeAccessNotification);
appTokenCache.SetAfterAccess(AfterAccessNotification);
}
return appTokenCache;
}
static TokenCache appTokenCache;
/// <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 を以下のコードと差し替え。テナント名など適宜書き換え。
using Microsoft.Identity.Client;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace GraphAppDemo
{
public partial class Program
{
private static string clientId = "550dcc01-129d-4b51-b80c-96995a6848e3";
private static string authority = "https://login.microsoftonline.com/graphdemo01.onmicrosoft.com/v2.0";
private static string redirectUri = "http://localhost";
private static Uri graphUrl = new Uri("https://graph.microsoft.com/");
private static string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
private static string secret = "hSL3....";
private static ConfidentialClientApplication cca;
static void Main(string[] args)
{
cca = new ConfidentialClientApplication(clientId, authority,
redirectUri, new ClientCredential(secret), null, TokenCacheHelper.GetAppCache());
Program p = new Program();
p.Run().Wait();
Console.ReadLine();
}
private async Task Run()
{
using (HttpClient client = new HttpClient())
{
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
client.BaseAddress = graphUrl;
var result = await client.GetAsync("/v1.0/users");
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;
authResult = await cca.AcquireTokenForClientAsync(scopes);
if (authResult != null)
return authResult.AccessToken;
else
return null;
}
}
}
6. F5 キーを押下してプログラムを実行。
コードの詳細確認
ではコードの詳細を確認していきます。
スコープ
アプリケーション認証の場合のスコープは、ユーザー認証と異なり、https://graph.microsoft.com/.default 1 つだけとなります。これで登録したすべての権限を要求します。
認証
まず MSAL クライアントを作成。アプリケーション認証をする場合は、ConfidentialClientApplication を利用します。
引数として
- clientId : 登録したアプリケーション ID
- authority : ログイン先テナントを指定した login.microsoftonline.com アドレス
- redirectUri : Web プラットフォームに登録したリダイレクト URL
- ClientCredential : 取得したパスワードを指定して作成
- UserTokenCache/AppTokenCache: ユーザートークンおよびアプリケーショントークンのキャッシュ
authClient = new ConfidentialClientApplication(clientId, authority, redirectUri, new ClientCredential(secret), null, TokenCacheHelper.GetAppCache());
次にアクセストークン取得です。アプリケーション認証のトークンは、AcquireTokenForClientAsync メソッドにスコープのみ指定して実行。
authClient.AcquireTokenForClientAsync(scopes)
Microsoft Graph の呼び出し
HttpClient を使って Graph を呼び出しています。Graph の C# SDK はまた別の機会に紹介します。
タイトルを更新
権限も付与済ですので、ユーザーのタイトルを更新してみます。
1. Run メソッドを以下のコードと差し替え。
private async Task Run()
{
using (HttpClient client = new HttpClient())
{
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
client.BaseAddress = graphUrl;
var result = await client.GetAsync("/v1.0/users?$select=id,displayName,jobTitle");
var users = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync());
foreach (var user in (users as JToken)["value"] as JArray)
{
var id = user.Value<string>("id");
user["jobTitle"] = "アーキテクト";
var request = new HttpRequestMessage(new HttpMethod("PATCH"), $"/v1.0/users/{id}");
request.Content = new StringContent(JsonConvert.SerializeObject(user), System.Text.Encoding.UTF8, "application/json");
result = await client.SendAsync(request);
}
result = await client.GetAsync("/v1.0/users?$select=id,displayName,jobTitle");
Console.WriteLine(JsonConvert.SerializeObject(
JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
Formatting.Indented));
}
}
2. F5 キーを押下して実行。結果を確認。
まとめ
アプリケーション認証は管理者同意が必要な強力な権限を必要としますが、バックグランドサービスとして動かしておくシナリオでは必須です。
参照
ユーザーなしでアクセスを取得
Azure Active Directory v2.0 と OAuth 2.0 クライアント資格情報フロー
Microsoft Graph Client Library for .NET