#デバイスのプロファイル認証
.NET Core コンソールアプリケーションやキーボードもタッチスクリーンもないデバイスなど、ユーザーがサインインする手段が提供できない環境があった場合、デバイスプロファイル認証が利用できます。
このフローでは認証が必要な場合、アプリケーションは認証先のアドレスとデバイス ID を提示し、ユーザーは指示に従って別のコンピューターなどで認証を完了すると、その情報が元のアプリケーション側に通達されます。
今回は以下のアプリケーションを作ります。
- .NET Core 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 で .NET Core C# のコンソールアプリケーションプロジェクトを作成。
2. NuGet の管理で 「Microsoft.IdentityModel.Clients.ActiveDirectory (ADAL)」、「System.Security.Cryptography.ProtectedData」および「JSON.NET」を追加。
3. TokenCacheHelper.cs ファイルを追加し、以下のコードと差し替え。これは ADAL で取得したトークンキャッシュをローカルディスクに保存するためのクラス。
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO;
using System.Security.Cryptography;
namespace GraphDeviceDemo
{
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;
}
}
}
}
}
}
4. 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 GraphDeviceDemo
{
public partial class Program
{
private static AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common/", TokenCacheHelper.GetTokenCache());
private static string clientId = "dd2a658e-8127-4956-ab57-bc203ad7b77b";
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
{
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)
{
DeviceCodeResult codeResult = await authContext.AcquireDeviceCodeAsync(graphUrl, clientId);
Console.WriteLine("サインインが必要です。");
Console.WriteLine($"{codeResult.VerificationUrl} にアクセスして、デバイスコード {codeResult.UserCode} を指定してログインしてください。");
authResult = await authContext.AcquireTokenByDeviceCodeAsync(codeResult);
}
}
catch (Exception ex)
{
Console.WriteLine("Something went wrong.");
Console.WriteLine("Message: " + ex.Message + "\n");
}
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));
}
}
}
}
5. F5 キーを押下してプログラムを実行。画面にメッセージが出る。
6. 指定されたアドレスにアクセスしてデバイスコードを入力。「続行」をクリック。
7. アクセス許可で「承諾」をクリック。
8. 結果が取得できていることを確認。
9. アプリケーションを再起動しても、トークンがキャッシュされているため認証は聞かれないことを確認。
コードの詳細確認
AuthenticationContext
ADAL の基本クラスである AuthenticationContext に、認証で使う Authority の値と、トークンキャッシュを渡します。今回はキャッシュをローカルに保存できるよう独自クラスを実装してるため、そちらを利用。
認証/認可
AcquireTokenSilentAsync メソッドはユーザーサインインを求めずトークンの取得を試みます。キャッシュがある場合はこれだけでトークンが取得できます。失敗した場合、AdalSilentTokenAcquisitionException 例外が発生します。
await pca.AcquireTokenSilentAsync(scopes, pca.Users.FirstOrDefault());
AcquireDeviceCodeAsync はデバイスプロファイル認証フローで必要な情報を取得します。結果を画面に出して、ユーザーがログインできるようにします。
DeviceCodeResult codeResult = await authContext.AcquireDeviceCodeAsync(graphUrl, clientId);
Console.WriteLine("サインインが必要です。");
Console.WriteLine($"{codeResult.VerificationUrl} にアクセスして、デバイスコード {codeResult.UserCode} を指定してログインしてください。");
AcquireTokenByDeviceCodeAsync を実行して、ユーザーがログインするのを待ちます。ログインが完了したら結果が返ります。
authResult = await authContext.AcquireTokenByDeviceCodeAsync(codeResult);
取得できるトークン
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));
}
予定が取得できています。
まとめ
デバイスプロファイル認証はクロスプラットフォームで動作するコマンドラインアプリケーションや IoT デバイス、またキオスクシナリオなどで活用できる機能です。Azure CLI もこの方式で認証をしています。是非試してください。
参照
Azure ポータル
Azure AD の認証シナリオ
GitHub:active-directory-dotnet-deviceprofile
Token cache serialization
Azure Active Directory のコード サンプル (V1 エンドポイント)