异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时

时间:2023-02-27
本文介绍了异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

我已按照 Microsoft

第二个令牌请求使用单线程:

  1. GetKeyAsync() 在 ThreadASP 上调用 GetAccessToken()(不在单独的线程上.)
  2. GetKeyAsync() 返回一个Task.
  3. 我们调用 GetResult() 阻塞 ThreadASP,直到 GetKeyAsync() 完成.
  4. GetAccessToken() 必须等到 ThreadASP 空闲,ThreadASP 必须等到 GetKeyAsync() 完成,GetKeyAsync() 必须等到 GetAccessToken() 完成.哦哦.
  5. 死锁.

为什么?谁知道?!?

GetKeyAsync() 中必须有一些依赖于访问令牌缓存状态的流控制.流控制决定是否在自己的线程上运行GetAccessToken(),以及在什么时候返回Task.

解决方案:一直异步

为避免死锁,最佳做法是一直使用异步".当我们调用来自外部库的异步方法时尤其如此,例如 GetKeyAsync().重要的是不要强制方法与 Wait()结果,或<代码>GetResult().相反,请使用 asyncawait 因为 await 会暂停方法而不是阻塞整个线程.

异步控制器动作

公共类 HomeController : 控制器{公共异步任务<ActionResult>指数(){var provider = new EncryptionProvider();等待提供者.GetKeyBundle();var x = provider.MyKeyBundle;返回视图();}}

异步公共方法

由于构造函数不能是异步的(因为异步方法必须返回一个Task),我们可以将异步的东西放到一个单独的公共方法中.

公共类 EncryptionProvider{////认证属性省略公共 KeyBundle MyKeyBundle;公共加密提供者(){}公共异步任务 GetKeyBundle(){var keyVaultClient = new KeyVaultClient(GetAccessToken);var keyBundleTask = 等待 keyVaultClient.GetKeyAsync(KeyVaultUrl, KeyVaultEncryptionKeyName);MyKeyBundle = keyBundleTask;}私有异步任务<字符串>获取访问令牌(字符串权限、字符串资源、字符串范围){TokenCache.DefaultShared.Clear();//重现问题var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);var clientCredential = new ClientCredential(ClientIdWeb, ClientSecretWeb);var 结果 = 等待 authContext.AcquireTokenAsync(resource, clientCredential);var token = result.AccessToken;返回令牌;}}

谜团解开了.:) 这是最终参考我的理解.

控制台应用

我最初的答案是这个控制台应用程序.它作为最初的故障排除步骤.它没有重现问题.

控制台应用每五分钟循环一次,反复请求新的访问令牌.在每个循环中,它都会输出当前时间、到期时间和检索到的密钥的名称.

在我的机器上,控制台应用程序运行了 1.5 小时,并在原始过期后成功检索到密钥.

使用系统;使用 System.Collections.Generic;使用 System.Threading.Tasks;使用 Microsoft.Azure.KeyVault;使用 Microsoft.IdentityModel.Clients.ActiveDirectory;命名空间 ConsoleApp{课堂节目{私有静态异步任务 RunSample(){var keyVaultClient = new KeyVaultClient(GetAccessToken);//创建一个密钥 :)var keyCreate = 等待 keyVaultClient.CreateKeyAsync(保险库:_keyVaultUrl,密钥名称:_keyVaultEncryptionKeyName,密钥类型:_keyType,关键属性:新的关键属性(){启用 = 真,过期 = UnixEpoch.FromUnixTime(int.MaxValue),NotBefore = UnixEpoch.FromUnixTime(0),},技术标签: 新字典<字符串、字符串>{{ "目的", "StackOverflow 演示" }});Console.WriteLine(string.Format(已创建 {0}",keyCreate.KeyIdentifier.Name));//取回密钥var keyRetrieve = 等待 keyVaultClient.GetKeyAsync(_keyVaultUrl,_keyVaultEncryptionKeyName);Console.WriteLine(string.Format("检索到 {0}",keyRetrieve.KeyIdentifier.Name));}私有静态异步任务<字符串>获取访问令牌(字符串权限、字符串资源、字符串范围){var clientCredential = 新的 ClientCredential(_keyVaultAuthClientId,_keyVaultAuthClientSecret);var context = new AuthenticationContext(权威,TokenCache.DefaultShared);var result = await context.AcquireTokenAsync(resource, clientCredential);_expiresOn = 结果.ExpiresOn.DateTime;Console.WriteLine(DateTime.UtcNow.ToShortTimeString());Console.WriteLine(_expiresOn.ToShortTimeString());返回结果.AccessToken;}私有静态日期时间_expiresOn;私有静态字符串_keyVaultAuthClientId = "xxxxx-xxx-xxxxx-xxx-xxxxx",_keyVaultAuthClientSecret = "xxxxx-xxx-xxxxx-xxx-xxxxx",_keyVaultEncryptionKeyName = "MYENCRYPTIONKEY",_keyVaultUrl = "https://xxxxx.vault.azure.net/",_keyType = "RSA";静态无效主要(字符串 [] 参数){var keepGoing = true;同时(继续){RunSample().GetAwaiter().GetResult();//休眠五分钟System.Threading.Thread.Sleep(new TimeSpan(0, 5, 0));如果(日期时间.UtcNow > _expiresOn){Console.WriteLine("---过期---");Console.ReadLine();}}}}}

I have setup Azure Keyvault on my ASP.Net MVC web application by following the example in Microsoft's Hello Key Vault sample application.

Azure KeyVault (Active Directory) AuthenticationResult by default has a one hour expiry. So after one hour, you must get a new authentication token. KeyVault is working as expected for the first hour after getting my first AuthenticationResult token, but after the 1 hour expiry, it fails to get a new token.

Unfortunately it took a failure on my production environment for me to realize this, as I never tested past one hour in development.

Anyways, after over two days of trying to figure out what was wrong with my keyvault code, I came up with a solution that fixes all of my problems - remove the asynchronous code - but it feels very hacky. I want to find out why it was not working in the first place.

My code looks like this:

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

The GetAccessToken method signature has to be asynchronous to pass into the new KeyVaultClient constructor, so I left the signature as async, but I removed the await keyword.

With the await keyword in there (the way it should be, and is in the sample):

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

The program works fine the first time I run it. And for an hour, AcquireTokenAsync returns the same original authentication token which is great. But once the token expires, AcquiteTokenAsync should get a new token with a new expiry date. And it doesn't - the application just hangs. No error returned, nothing at all.

So calling AcquireToken instead of AcquireTokenAsync solves the problem, but I have no idea why. You'll also notice that I'm passing 'null' instead of 'TokenCache.DefaultShared' into the AuthenticationContext constructor in my sample code with async. This is to force the toke to expire immediately instead of after one hour. Otherwise, you have to wait an hour to reproduce the behavior.

I was able to reproduce this again in a brand new MVC project, so I don't think it has anything to do with my specific project. Any insight would be appreciated. But for now, I'm just not using async.

解决方案

Problem: deadlock

Your EncryptionProvider() is calling GetAwaiter().GetResult(). This blocks the thread, and on subsequent token requests, causes a deadlock. The following code is the same as yours is but separates things to facilitate explanation.

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

In both token requests, the execution starts in the same way:

  • AzureEncryptionProvider() runs in what we'll call ThreadASP.
  • AzureEncryptionProvider() calls GetKeyAsync().

Then things differ. The first token request is multi-threaded:

  1. GetKeyAsync() returns a Task.
  2. We call GetResult() blocking ThreadASP until GetKeyAsync() completes.
  3. GetKeyAsync() calls GetAccessToken() on another thread.
  4. GetAccessToken() and GetKeyAsync() complete, freeing ThreadASP.
  5. Our web page returns to the user. Good.

The second token request uses a single thread:

  1. GetKeyAsync() calls GetAccessToken() on ThreadASP (not on a separate thread.)
  2. GetKeyAsync() returns a Task.
  3. We call GetResult() blocking ThreadASP until GetKeyAsync() completes.
  4. GetAccessToken() must wait until ThreadASP is free, ThreadASP must wait until GetKeyAsync() completes, GetKeyAsync() must wait until GetAccessToken() completes. Uh oh.
  5. Deadlock.

Why? Who knows?!?

There must be some flow control within GetKeyAsync() that relies on the state of our access token cache. The flow control decides whether to run GetAccessToken() on its own thread and at what point to return the Task.

Solution: async all the way down

To avoid a deadlock, it is a best practice "to use async all the way down." This is especially true when we are calling an async method, such as GetKeyAsync(), that is from an external library. It is important not force the method to by synchronous with Wait(), Result, or GetResult(). Instead, use async and await because await pauses the method instead of blocking the whole thread.

Async controller action

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

Async public method

Since a constructor cannot be async (because async methods must return a Task), we can put the async stuff into a separate public method.

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

Mystery solved. :) Here is a final reference that helped my understanding.

Console App

My original answer had this console app. It worked as an initial troubleshooting step. It did not reproduce the problem.

The console app loops every five minutes, repeatedly asking for a new access token. At each loop, it outputs the current time, the expiry time, and the name of the retrieved key.

On my machine, the console app ran for 1.5 hours and successfully retrieved a key after expiration of the original.

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

这篇关于异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!

上一篇:使用电子邮件地址和应用程序密码从 oauth2/token 获取访问令牌 下一篇:.Net Core 2.0 - 获取 AAD 访问令牌以与 Microsoft Graph 一起使用

相关文章

最新文章