Подтвердить что ты не робот

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

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

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

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

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

Мой код выглядит следующим образом:

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, поэтому я оставил подпись как async, но я удалил ключевое слово ожидания.

С ключевым словом ожидания (там, где он должен быть, и находится в образце):

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 решает проблему, но я понятия не имею, почему. Вы также заметите, что я передаю "null" вместо "TokenCache.DefaultShared" в конструктор AuthenticationContext в моем примере кода с помощью async. Это означает, что токек истекает немедленно, а не через час. В противном случае вам нужно подождать один час, чтобы воспроизвести поведение.

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

4b9b3361

Ответ 1

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

Ваш 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().

Тогда все по-другому. Первый запрос на токен многопоточен:

  • GetKeyAsync() возвращает Task.
  • Мы вызываем GetResult(), блокируя ThreadASP до тех пор, пока GetKeyAsync() не завершится.
  • GetKeyAsync() вызывает GetAccessToken() в другом потоке.
  • GetAccessToken() и GetKeyAsync() завершение, освобождение ThreadASP.
  • Наша веб-страница возвращается пользователю. Хорошо.

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

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

  • GetKeyAsync() вызывает GetAccessToken() в ThreadASP (не в отдельном потоке.)
  • GetKeyAsync() возвращает a Task.
  • Мы вызываем GetResult(), блокируя ThreadASP до тех пор, пока GetKeyAsync() не завершится.
  • GetAccessToken() должен ждать, пока ThreadASP не будет освобожден, ThreadASP должен дождаться завершения GetKeyAsync(), GetKeyAsync() должен дождаться завершения GetAccessToken(). Ох. ​​
  • Тупик.

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

Почему? Кто знает?!?

В GetKeyAsync() должно быть некоторое управление потоком, которое зависит от состояния кеша токена доступа. Управление потоком решает, следует ли запускать GetAccessToken() в своем потоке и в какой точке вернуть Task.

Решение: асинхронно до конца

Чтобы избежать тупика, лучше всего использовать async до конца. Это особенно актуально, когда мы вызываем метод async, например GetKeyAsync(), который находится из внешней библиотеки. Важно не форсировать метод синхронно с Wait(), Result, или GetResult(). Вместо этого используйте async и await, потому что await приостанавливает этот метод вместо блокировки всего потока.

Действие контроллера Async

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

Открытый метод Async

Поскольку конструктор не может быть асинхронным (поскольку методы async должны возвращать a Task), мы можем поместить материал async в отдельный публичный метод.

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", "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();
                }
            }
        }
    }
}

Ответ 2

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

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

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

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

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