概要
AzureサービスのSDKを使ってAzureの各サービスを呼び出すとき、
①パブリッククライアントであるWindowsデスクトップアプリから、
②ユーザー自身のEntraIDを使ってAzureサービスに接続する方法
よくある認証方法のコード(DefaultAzureCredential)
- Azureサービスを呼び出すSDKのサンプルコードによくある形
- TokenCredentialとして
DefaultAzureCredential
を指定する方法です
string keyVaultName = Environment.GetEnvironmentVariable("KEY_VAULT_NAME");
var kvUri = "https://" + keyVaultName + ".vault.azure.net";
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
この方法は、ドキュメントにある通り、主に開発環境とシークレットクライアント(サーバーサイドアプリなど)の環境で認証させる方法です。
環境変数やマネージドIDを使って認証させる方法なので、仮想マシンやAzureサービスから呼び出すことが前提です。
ちなみにDefaultAzureCredential及びその基底クラスであるTokenCredentialですが、
インスタンス作成時点や、上記コードのSecretClientコンストラクタの時点では何も起きません。
実際にAzureサービスを呼び出すタイミングで、TokenCredential.GetTokenAsync()が呼び出され、認証される仕組みです。
Azure.IdentityライブラリはMSALを内包しており、アクセストークンの取得やトークンリフレッシュを行ってくれるようです。
ユーザーのEntra IDを使って認証する
事前準備
- サービスを利用させるユーザーに対してIAMでロールを割り当てておきます
- 今回の例のAzure AI Documentでは、利用者に対して
Cognitive Services ユーザー
の割り当てが必要でした
DefaultAzureCredentialとInteractiveBrowserCredentialオプション
DefaultAzureCredentialのオプションとして、ブラウザを使った対話型画面によりユーザーのEntraIDを使用した認証に対応可能です。 ただし既定では無効になっているので、コンストラクタの引数で有効にしてあげる必要があります。
また、他の認証方法を利用しない場合は以下のようにカスタムして無効にできます。
var credential = new DefaultAzureCredential(
new DefaultAzureCredentialOptions()
{
// デスクトップアプリでは使わない認証方法は無効に
ExcludeAzurePowerShellCredential = true,
ExcludeEnvironmentCredential = true,
ExcludeWorkloadIdentityCredential = true,
ExcludeManagedIdentityCredential = true,
ExcludeAzureDeveloperCliCredential = true,
ExcludeAzureCliCredential = true,
// キャッシュとブラウザを有効
ExcludeSharedTokenCacheCredential = false,
ExcludeInteractiveBrowserCredential = false,
}
);
ただし、各認証方法のoptionsが指定できないので、トークン情報のディスクキャッシュが有効になるかは不明です。
ネイティブ認証ブローカーを使った認証
こちらの方がおすすめの方法です。
WindowsのWAM(Web Account Manager)を使って対話型画面で認証させます。
以下の追加ライブラリが必要です
Azure.Identity.Broker
コードは以下
GetCredentialメソッドを作成しておきます
public static TokenCredential GetCredential(nint handle)
{
TokenCachePersistenceOptions tokenCachePersistenceOptions = new();
TokenCredential credential = new ChainedTokenCredential(
// ディスクキャッシュからの読み出し
new SharedTokenCacheCredential(
// 必ずBroker版を指定
new SharedTokenCacheCredentialBrokerOptions(tokenCachePersistenceOptions)
),
// ネイティブ認証ブローカー
new InteractiveBrowserCredential(
new InteractiveBrowserCredentialBrokerOptions(handle){
TokenCachePersistenceOptions = tokenCachePersistenceOptions
}
)
);
return credential;
}
- ChainedTokenCredential を使うと、任意の認証方法を順番に試行するTokenCredentialを作成可能です
- 上記の設定ではトークンのディスクキャッシュの読み込みをトライして、ダメなら認証画面を表示する流れです
- なので、初回はWAMを利用してユーザーのEntra IDを選択またはログインさせて認証します
- handleには親ウィンドウのウィンドウハンドルを指定します
- TokenCachePersistenceOptions を使うことでトークン情報をディスクにキャッシュして2回目以降はサイレントにトークン情報を取得します
'24/12/19追記
SharedTokenCacheCredentialの書き方に誤りがありましたので修正しました。
そして、Azureサービスを呼び出すときは以下のように呼び出せます。 以下はDocumentIntelligenceの例です。
// TokenCredentialを作成
TokenCredential credential = GetCredential(brokerParentHandle);
// クライアント初期化
var client = new DocumentAnalysisClient(new Uri($"https://{_resource}.cognitiveservices.azure.com/"), credential);
using var stream = new FileStream(filePath, FileMode.Open);
AnalyzeDocumentOperation operation;
try
{
// AI Document呼び出し
operation = await _client.AnalyzeDocumentAsync(WaitUntil.Completed, "prebuilt-read", stream);
}
catch (AuthenticationFailedException e)
{
// 認証に失敗したとき
Console.WriteLine($"Authentication Failed. {e.Message}");
}
ブラウザを使った認証方法もあるのですが、ユーザーが誤ってタブを閉じると永遠にデスクトップアプリ側に制御が戻らない不具合があり、実用上の問題がありそうでした。
おまけ
トークンキャッシュの中身を確認する
- MSALのキャッシュファイルを復号化して表示します。JSONです
- どうもWAMで認証させるとAccessTokenやRefreshTokenは空でIdTokenのみ保存されています
- IDTokenだけキャッシュしておいて、IdTokenを使って都度都度アクセストークンをサイレントに取得する感じ?
public static readonly string CacheFilePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".IdentityService", "msal.cache.cae");
public static string ReadTokenCache()
{
var byteArray = ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
null,
DataProtectionScope.CurrentUser);
string str = Encoding.UTF8.GetString(byteArray);
return str;
}
トークンキャッシュの保存先は以下
AppData\Local\.IdentityServiceのmsal.cache.cae
tokenCachePersistenceOptions の初期化子で名前を指定することでキャッシュのファイル名を変更できそうです。
トークンリクエスト時のアプリケーションIDやスコープ
- アプリケーションIdはAzure CLIのものを利用しているよう
- スコープはAzureサービスのSDKから取得し、AIサービスでは以下が指定される
public const string DefaultCognitiveScope = "https://cognitiveservices.azure.com/.default";
- このスコープを指定してアクセストークンを取得しているようだ
おまけ2
認証ブローカーで保存したトークンキャッシュからIAccountを取り出す方法
- Azure.IdentityやMicrosoft.Identity.Client.Extensions.Msalのソースコードから見つけた
- ポイントは
WithBroker()
で、認証ブローカーから取得したトークンキャッシュはこれがないと取得できない- なのでこのコードはAzure.Identity.Brokerに書いてある
- これができれば自分でアクセストークンも取得できる
public static async Task<IAccount> ReadAccount()
{
// msalキャッシュヘルパー、今はMSALに内包されているらしい
var strageProperties = new StorageCreationPropertiesBuilder("msal.cache.cae", Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".IdentityService"))
.Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(strageProperties);
// 書き込みが有効かのテスト
cacheHelper.VerifyPersistence();
// Msalクライアントを準備
// AppIdはAzure CLIのもの、その他はソースコードを真似
var pca = PublicClientApplicationBuilder.Create("04b07795-8ddb-461a-bbee-02f9e1bf7b46")
.WithAuthority(@"https://login.microsoftonline.com/", "organizations", false)
.WithClientCapabilities(new List<string>() { "CP1" })
// Windows認証ブローカーを使用した場合は以下オプションが必要
.WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows))
.Build();
// キャッシュ読み書き時のイベントを登録
cacheHelper.RegisterCache(pca.UserTokenCache);
// アカウント取得
var accounts = await pca.GetAccountsAsync();
return accounts.FirstOrDefault();
}
参考