web-dev-qa-db-ja.com

非同期で呼び出された場合のAzureKeyVault Active DirectoryAcquireTokenAsyncタイムアウト

Microsoftの Hello Key Vault サンプルアプリケーションの例に従って、ASP.Net MVCWebアプリケーションにAzureKeyvaultをセットアップしました。

Azure KeyVault(Active Directory)AuthenticationResultの有効期限はデフォルトで1時間です。したがって、1時間後、新しい認証トークンを取得する必要があります。 KeyVaultは、最初のAuthenticationResultトークンを取得してから最初の1時間は期待どおりに機能していますが、1時間の有効期限が切れると、新しいトークンを取得できません。

残念ながら、開発で1時間以上テストしたことがなかったため、これを実現するために本番環境で失敗しました。

とにかく、keyvaultコードの何が問題なのかを2日以上調べた後、すべての問題を修正するソリューションを思いつきました-非同期コードを削除します-しかし、それは非常にハックな感じがします。そもそもなぜうまくいかなかったのか知りたい。

私のコードは次のようになります:

public AzureEncryptionProvider() //class constructor
{
   _keyVaultClient = new KeyVaultClient(GetAccessToken);
   _keyBundle = _keyVaultClient
     .GetKeyAsync(_keyVaultUrl, _keyVaultEncryptionKeyName)
     .GetAwaiter().GetResult();
}

private static readonly string _keyVaultAuthClientId = 
    ConfigurationManager.AppSettings["KeyVaultAuthClientId"];

private static readonly string _keyVaultAuthClientSecret =
    ConfigurationManager.AppSettings["KeyVaultAuthClientSecret"];

private static readonly string _keyVaultEncryptionKeyName =
    ConfigurationManager.AppSettings["KeyVaultEncryptionKeyName"];

private static readonly string _keyVaultUrl = 
    ConfigurationManager.AppSettings["KeyVaultUrl"];

private readonly KeyBundle _keyBundle;
private readonly KeyVaultClient _keyVaultClient;

private static async Task<string> GetAccessToken(
    string authority, string resource, string scope)
{
   var clientCredential = new ClientCredential(
       _keyVaultAuthClientId, 
       _keyVaultAuthClientSecret);
   var context = new AuthenticationContext(
       authority, 
       TokenCache.DefaultShared);
   var result = context.AcquireToken(resource, clientCredential);
   return result.AccessToken;
}

GetAccessTokenメソッドの署名は、新しいKeyVaultClientコンストラクターに渡すために非同期である必要があるため、署名を非同期のままにしましたが、awaitキーワードを削除しました。

そこにawaitキーワードがある場合(あるべき姿であり、サンプルにあります):

private static async Task<string> GetAccessToken(string authority, string resource, string scope)
{
   var clientCredential = new ClientCredential(_keyVaultAuthClientId, _keyVaultAuthClientSecret);
   var context = new AuthenticationContext(authority, null);
   var result = await context.AcquireTokenAsync(resource, clientCredential);
   return result.AccessToken;
}

プログラムは、初めて実行したときに正常に動作します。そして1時間、AcquireTokenAsyncは同じ元の認証トークンを返します。これは素晴らしいことです。ただし、トークンの有効期限が切れると、AcquiteTokenAsyncは新しい有効期限の新しいトークンを取得する必要があります。そして、そうではありません-アプリケーションがハングするだけです。エラーは返されず、まったく何も返されませんでした。

したがって、AcquireTokenAsyncの代わりにAcquireTokenを呼び出すと問題は解決しますが、理由はわかりません。また、asyncを使用してサンプルコードのAuthenticationContextコンストラクターに「TokenCache.DefaultShared」ではなく「null」を渡していることにも気付くでしょう。これは、1時間後ではなく、すぐにトークを強制的に期限切れにするためです。それ以外の場合は、動作を再現するために1時間待つ必要があります。

これをまったく新しいMVCプロジェクトで再現できたので、特定のプロジェクトとは何の関係もないと思います。任意の洞察をいただければ幸いです。しかし今のところ、私は非同期を使用していません。

15
Aaron

問題:デッドロック

EncryptionProvider()が呼び出しています GetAwaiter().GetResult() 。これによりスレッドがブロックされ、後続のトークン要求でデッドロックが発生します。次のコードはあなたのものと同じですが、説明を容易にするために物事を分離しています。

_public AzureEncryptionProvider() // runs in ThreadASP
{
    var client = new KeyVaultClient(GetAccessToken);

    var task = client.GetKeyAsync(KeyVaultUrl, KeyVaultEncryptionKeyName);

    var awaiter = task.GetAwaiter();

    // blocks ThreadASP until GetKeyAsync() completes
    var keyBundle = awaiter.GetResult();
}
_

どちらのトークン要求でも、実行は同じ方法で開始されます。

  • AzureEncryptionProvider()は、ThreadASPと呼ばれるもので実行されます。
  • AzureEncryptionProvider()GetKeyAsync()を呼び出します。

それから物事は異なります。最初のトークンリクエストはマルチスレッドです:

  1. GetKeyAsync()Task を返します。
  2. GetResult()GetKeyAsync()が完了するまでThreadASPをブロックすることを呼び出します。
  3. GetKeyAsync()は別のスレッドでGetAccessToken()を呼び出します。
  4. GetAccessToken()およびGetKeyAsync()が完了し、ThreadASPが解放されます。
  5. 私たちのウェブページがユーザーに戻ります。良い。

GetAccessToken is running on its own thread.

2番目のトークン要求は単一のスレッドを使用します。

  1. GetKeyAsync()はThreadASPでGetAccessToken()を呼び出します(別のスレッドではありません)。
  2. GetKeyAsync()Taskを返します。
  3. GetResult()が完了するまで、GetKeyAsync()ブロッキングThreadASPを呼び出します。
  4. GetAccessToken()はThreadASPが解放されるまで待機する必要があり、ThreadASPはGetKeyAsync()が完了するまで待機する必要があり、GetKeyAsync()GetAccessToken()が完了するまで待機する必要があります。ええとああ。
  5. デッドロック。

GetAccessToken is running on the same thread.

どうして?知るか?!?

GetKeyAsync()内には、アクセストークンキャッシュの状態に依存するフロー制御が必要です。フロー制御は、独自のスレッドでGetAccessToken()を実行するかどうか、およびどの時点でTaskを返すかを決定します。

解決策:完全に非同期

デッドロックを回避するには、「非同期を完全に使用する」ことをお勧めします。これは、外部ライブラリからGetKeyAsync()などの非同期メソッドを呼び出す場合に特に当てはまります。 Wait()Result 、またはGetResult()と同期してメソッドを強制しないことが重要です。代わりに、 asyncおよびawait を使用してください。これは、awaitがスレッド全体をブロックするのではなく、メソッドを一時停止するためです。

非同期コントローラーアクション

_public class HomeController : Controller
{
    public async Task<ActionResult> Index()
    {
        var provider = new EncryptionProvider();
        await provider.GetKeyBundle();
        var x = provider.MyKeyBundle;
        return View();
    }
}
_

非同期パブリックメソッド

コンストラクターを非同期にすることはできないため(非同期メソッドはTaskを返す必要があるため)、非同期のものを別のパブリックメソッドに入れることができます。

_public class EncryptionProvider
{
    //
    // authentication properties omitted

    public KeyBundle MyKeyBundle;

    public EncryptionProvider() { }

    public async Task GetKeyBundle()
    {
        var keyVaultClient = new KeyVaultClient(GetAccessToken);
        var keyBundleTask = await keyVaultClient
            .GetKeyAsync(KeyVaultUrl, KeyVaultEncryptionKeyName);
        MyKeyBundle = keyBundleTask;
    }

    private async Task<string> GetAccessToken(
        string authority, string resource, string scope)
    {
        TokenCache.DefaultShared.Clear(); // reproduce issue 
        var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
        var clientCredential = new ClientCredential(ClientIdWeb, ClientSecretWeb);
        var result = await authContext.AcquireTokenAsync(resource, clientCredential);
        var token = result.AccessToken;
        return token;
    }
}
_

謎が解けた。 :)ここに 最終参照 私の理解を助けました。

コンソールアプリ

私の最初の答えはこのコンソールアプリでした。これは、最初のトラブルシューティング手順として機能しました。 問題は再現されませんでした。

コンソールアプリは5分ごとにループし、新しいアクセストークンを繰り返し要求します。各ループで、現在の時刻、有効期限、および取得したキーの名前を出力します。

私のマシンでは、コンソールアプリが1.5時間実行され、元のアプリの有効期限が切れた後、キーを正常に取得しました。

_using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace ConsoleApp
{
    class Program
    {
        private static async Task RunSample()
        {
            var keyVaultClient = new KeyVaultClient(GetAccessToken);

            // create a key :)
            var keyCreate = await keyVaultClient.CreateKeyAsync(
                vault: _keyVaultUrl,
                keyName: _keyVaultEncryptionKeyName,
                keyType: _keyType,
                keyAttributes: new KeyAttributes()
                {
                    Enabled = true,
                    Expires = UnixEpoch.FromUnixTime(int.MaxValue),
                    NotBefore = UnixEpoch.FromUnixTime(0),
                },
                tags: new Dictionary<string, string> {
                    { "purpose", "StackOverflow Demo" }
                });

            Console.WriteLine(string.Format(
                "Created {0} ",
                keyCreate.KeyIdentifier.Name));

            // retrieve the key
            var keyRetrieve = await keyVaultClient.GetKeyAsync(
                _keyVaultUrl,
                _keyVaultEncryptionKeyName);

            Console.WriteLine(string.Format(
                "Retrieved {0} ",
                keyRetrieve.KeyIdentifier.Name));
        }

        private static async Task<string> GetAccessToken(
            string authority, string resource, string scope)
        {
            var clientCredential = new ClientCredential(
                _keyVaultAuthClientId,
                _keyVaultAuthClientSecret);

            var context = new AuthenticationContext(
                authority,
                TokenCache.DefaultShared);

            var result = await context.AcquireTokenAsync(resource, clientCredential);

            _expiresOn = result.ExpiresOn.DateTime;

            Console.WriteLine(DateTime.UtcNow.ToShortTimeString());
            Console.WriteLine(_expiresOn.ToShortTimeString());

            return result.AccessToken;
        }

        private static DateTime _expiresOn;
        private static string
            _keyVaultAuthClientId = "xxxxx-xxx-xxxxx-xxx-xxxxx",
            _keyVaultAuthClientSecret = "xxxxx-xxx-xxxxx-xxx-xxxxx",
            _keyVaultEncryptionKeyName = "MYENCRYPTIONKEY",
            _keyVaultUrl = "https://xxxxx.vault.Azure.net/",
            _keyType = "RSA";

        static void Main(string[] args)
        {
            var keepGoing = true;
            while (keepGoing)
            {
                RunSample().GetAwaiter().GetResult();
                // sleep for five minutes
                System.Threading.Thread.Sleep(new TimeSpan(0, 5, 0)); 
                if (DateTime.UtcNow > _expiresOn)
                {
                    Console.WriteLine("---Expired---");
                    Console.ReadLine();
                }
            }
        }
    }
}
_
24
Shaun Luttin