Тайм-аут Azure KeyVault Active Directory AcquireTokenAsync при асинхронном вызове

Я настроил Azure Keyvault в своем веб-приложении ASP.Net MVC, следуя примеру в примере приложения Hello Key Vault от Microsoft.

Azure KeyVault (Active Directory) AuthenticationResult по умолчанию имеет один час истечения. Поэтому через час вы должны получить новый токен аутентификации. KeyVault работает должным образом в течение первого часа после получения моего первого токена AuthenticationResult, но по истечении 1 часа он не может получить новый токен.

К сожалению, в моей производственной среде произошел сбой, и я понял это, поскольку я никогда не тестировал последние один час в разработке.

В любом случае, после более чем двух дней попыток выяснить, что не так с моим кодом keyvault, я нашел решение, которое исправляет все мои проблемы - удалить асинхронный код - но это кажется очень хакерским. Я хочу выяснить, почему это не сработало.

Мой код выглядит так:

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;
}

Программа работает нормально при первом запуске. И в течение часа AcquireTokenAsync возвращает тот же самый оригинальный маркер аутентификации, который великолепен. Но как только срок действия токена истекает, AcquiteTokenAsync должен получить новый токен с новой датой истечения срока действия. И это не так - приложение просто зависает. Ошибка не возвращается, вообще ничего.

Поэтому вызов AcquireToken вместо AcquireTokenAsync решает проблему, но я понятия не имею, почему. Вы также заметите, что я передаю 'нулевой' вместо 'TokenCache.DefaultShared' в конструктор AuthenticationContext в моем примере кода с помощью async. Это заставляет токен истечь немедленно, а не через час. В противном случае вам придется подождать час, чтобы воспроизвести поведение.

Мне удалось воспроизвести это снова в совершенно новом проекте MVC, поэтому я не думаю, что это имеет какое-либо отношение к моему конкретному проекту. Любое понимание будет оценено. Но сейчас я просто не использую async.

2 ответа

Решение

Проблема: тупик

Ваш 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() блокировка ThreadASP до GetKeyAsync() завершается.
  3. GetKeyAsync() звонки GetAccessToken() в другой теме.
  4. GetAccessToken() а также GetKeyAsync() полный, освобождающий ThreadASP.
  5. Наша веб-страница возвращается к пользователю. Хорошо.

GetAccessToken работает в своем собственном потоке

Второй запрос токена использует один поток:

  1. GetKeyAsync() звонки GetAccessToken() на ThreadASP (не на отдельном потоке.)
  2. GetKeyAsync() возвращает Task,
  3. Мы называем GetResult() блокировка ThreadASP до GetKeyAsync() завершается.
  4. GetAccessToken() должен подождать, пока ThreadASP освободится, ThreadASP должен подождать, пока GetKeyAsync() завершается, GetKeyAsync() должен ждать, пока GetAccessToken() завершается. Ооо
  5. Тупик.

GetAccessToken работает в том же потоке

Зачем? Кто знает?!?

Там должно быть некоторое управление потоком внутри 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;
    }
}

Тайна разгадана.:) Вот последняя ссылка, которая помогла моему пониманию.

Консольное приложение

У моего оригинального ответа было это консольное приложение. Это работало как начальный шаг устранения неполадок. Это не воспроизвело проблему.

Консольное приложение зацикливается каждые пять минут, постоянно запрашивая новый токен доступа. В каждом цикле выводится текущее время, время истечения и имя полученного ключа.

На моем компьютере консольное приложение работало 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", "Stackru 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();
                }
            }
        }
    }
}

У меня такой же вызов, как и у вас. Я предполагаю, что вы также видели образец, опубликованный по адресу https://azure.microsoft.com/en-us/documentation/articles/key-vault-use-from-web-application/

Существует большая разница между тем, что делает этот пример, и тем, что делает мой код (и я думаю, что смысл вашего кода). В этом примере они извлекают секрет и сохраняют его в веб-приложении как статический член своего класса Utils. Таким образом, образец извлекает секрет один раз за все время выполнения приложения.

В моем случае я получаю разные ключи для разных целей в разное время во время выполнения приложения.

Кроме того, в примере загрузки, на которую вы ссылаетесь, используется сертификат X.509 для аутентификации веб-приложения в KeyVault, а не секрет клиента. Возможно, с этим тоже есть проблема.

Я видел, как чат с @ shaun-luttin пришел к выводу, что вы зашли в тупик, но я думаю, это еще не вся история. Я не использую.GetAwaiter().GetResult() и не вызываю асинхронный метод из ctor.

Другие вопросы по тегам