使用MSI对Azure BlobStorage进行身份验证的问题

时间:2018-12-05 19:18:52

标签: c# azure .net-core azure-webjobs service-principal

我正在尝试将C#控制台应用程序(.net core 2.1)连接到Blob存储。我用两种不同的方式初始化Blob存储客户端。他们是:

  1. 连接字符串-在开发过程中很有用
  2. 服务原则-生产部署的可能性
  3. MSI身份验证-更加安全,密钥会自动为我们循环

在我的代码中,如果未显式设置连接字符串,则根据定义的应用程序设置,使用服务原理或MSI生成它(以下示例初始化代码)。不管我使用这三种方式中的哪一种,我最终都将使用连接字符串初始化客户端(或者在1的情况下显式设置,或者在2和3的情况下由我的代码生成)。

下面的代码对1(连接字符串)和2(服务原理)可以100%正常工作,但是尝试达到3(MSI)时出现错误。

在本地运行时,出现此错误:

  

访问令牌来自错误的发行者   'https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/'。它   必须与房客匹配   'https://sts.windows.net/ {my-subscription-id} /'   与此订阅相关联。请使用授权(URL)   'https://login.windows.net/ {my-subscription-id}'到   获取令牌。注意,如果订阅已转移到另一个   租户对服务没有影响,但是有关新租户的信息   租户可能要花一些时间才能传播(最多一个小时)。如果你只是   转移了您的订阅,并看到此错误消息,请尝试   以后再回来。

因此,我不知道“ f8cdef31-a31e-4b4a-93e4-5f571e91255a”来自何处,它可能是全球Microsoft实例。我试图通过在启用了MSI的Azure内的webjob中运行代码来缓解这种情况,我得到:

  

System.AggregateException:发生一个或多个错误。 (一个例外   服务连接期间发生,请参阅内部异常以获取更多信息   详细信息)---> System.Exception:服务期间发生异常   连接,请参阅内部异常以获取更多详细信息--->   Microsoft.Rest.Azure.CloudException:客户端“ {my-subscription-id}”   对象ID为“ {my-subscription-id}”的用户无权   在范围内执行操作“ Microsoft.Storage/storageAccounts/read”   “ / subscriptions / {my-subscription-id}”。

注意我将MSI帐户设置为Blob存储的“所有者”和“存储帐户密钥操作员”)

我通过以下方式初始化CloudStorageAccount客户端:

public void InitializeClient()
{
    // Always using the connection string, no matter how it's generated.
    if (ConnectionString.IsNullOrEmpty()) // if not already set, then build.
        ConnectionString = BuildStorageConnection().GetAwaiter().GetResult();

    CloudStorageAccount.TryParse(ConnectionString, out var storageAccount);

    if (storageAccount == null)
        throw new InvalidOperationException("Cannot find storage account");

    // CloudBlobClient that represents the Blob storage endpoint.
    _cloudBlobClient = storageAccount.CreateCloudBlobClient();
}

并按如下所示构建连接字符串:

internal async Task<string> BuildStorageConnection()
{
    try
    {
        string token = null;

        if (Config.UseMsi)
        {
            // Managed Service Identity (MSI) authentication.
            var provider = new AzureServiceTokenProvider();
            token = provider.GetAccessTokenAsync("https://management.azure.com/").GetAwaiter().GetResult();

            if (string.IsNullOrEmpty(token))
                throw new InvalidOperationException("Could not authenticate using Managed Service Identity");

            _expiryTime = DateTime.Now.AddDays(1);
        }
        else
        {
            // Service Principle authentication
            // Grab an authentication token from Azure.
            var context = new AuthenticationContext("https://login.windows.net/" + Config.TenantId);

            var credential = new ClientCredential(Config.AppId, Config.AppSecret);
            var tokenResult = context.AcquireTokenAsync("https://management.azure.com/", credential).GetAwaiter().GetResult();

            if (tokenResult == null || tokenResult.AccessToken == null)
                throw new InvalidOperationException($"Could not authenticate using Service Principle");

            _expiryTime = tokenResult.ExpiresOn;
            token = tokenResult.AccessToken;
        }

        // Set credentials and grab the authenticated REST client.
        var tokenCredentials = new TokenCredentials(token);

        var client = RestClient.Configure()
            .WithEnvironment(AzureEnvironment.AzureGlobalCloud)
            .WithLogLevel(HttpLoggingDelegatingHandler.Level.BodyAndHeaders)
            .WithCredentials(new AzureCredentials(tokenCredentials, tokenCredentials, string.Empty, AzureEnvironment.AzureGlobalCloud))
            .WithRetryPolicy(new RetryPolicy(new HttpStatusCodeErrorDetectionStrategy(), new FixedIntervalRetryStrategy(3, TimeSpan.FromMilliseconds(500))))
            .Build();

        // Authenticate against the management layer.
        var azureManagement = Azure.Authenticate(client, string.Empty).WithSubscription(Config.SubscriptionId);

        // Get the storage namespace for the passed in instance name.
        var storageNamespace = azureManagement.StorageAccounts.List().FirstOrDefault(n => n.Name == Config.StorageInstanceName);

        // If we cant find that name, throw an exception.
        if (storageNamespace == null)
        {
            throw new InvalidOperationException($"Could not find the storage instance {Config.StorageInstanceName} in the subscription with ID {Config.SubscriptionId}");
        }

        // Storage accounts use access keys - this will be used to build a connection string.
        var accessKeys = await storageNamespace.GetKeysAsync();

        // If the access keys are not found (not configured for some reason), throw an exception.
        if (accessKeys == null)
        {
            throw new InvalidOperationException($"Could not find access keys for the storage instance {Config.StorageInstanceName}");
        }

        // We just default to the first key.
        var key = accessKeys[0].Value;

        // Build and return the connection string.
        return $"DefaultEndpointsProtocol=https;AccountName={Config.StorageInstanceName};AccountKey={key};EndpointSuffix=core.windows.net";
    }
    catch (Exception e)
    {
        Logger?.LogError(e, "An exception occured during connection to blob storage");
        throw new Exception("An exception occurred during service connection, see inner exception for more detail", e);
    }
}

我如何获得访问令牌的主要区别在于,使用服务原理,我具有身份验证上下文,而使用MSI,我没有。这会影响认证范围吗?任何帮助和建议都非常感谢!

1 个答案:

答案 0 :(得分:2)

刚刚意识到了如何解决上述问题-将GetTokenAsync更改为具有TenantId的第二个参数将为身份验证调用提供上下文。

这是您需要的代码:

token = await provider.GetAccessTokenAsync("https://management.azure.com/", Config.TenantId);