Compartilhar via


Como aproveitar a identidade gerenciada de um aplicativo Service Fabric para acessar os serviços do Azure

Os aplicativos do Service Fabric poderão aproveitar as identidades gerenciadas para acessar outros recursos do Azure que dão suporte à autenticação baseada no Microsoft Entra ID. Um aplicativo pode obter um token de acesso representando a respectiva identidade, que pode ser atribuído pelo sistema ou pelo usuário, e usá-lo como um token de 'portador' para se autenticar em outro serviço, também conhecido como servidor de recursos protegido. O token representa a identidade atribuída ao aplicativo do Service Fabric e será emitido somente para os recursos do Azure (incluindo os aplicativos do SF) que compartilham essa identidade. Confira a documentação de visão geral da identidade gerenciada para obter uma descrição detalhada das identidades gerenciadas, bem como a distinção entre as identidades atribuídas pelo sistema e pelo usuário. Vamos nos referir a um aplicativo do Service Fabric habilitado para identidade gerenciada como o aplicativo cliente ao longo deste artigo.

Confira um aplicativo complementar de exemplo que demonstra o uso de identidades gerenciadas de aplicativo do Service Fabric atribuídas pelo sistema e pelo usuário com Reliable Services e contêineres.

Importante

Uma identidade gerenciada representa a associação entre um recurso do Azure e uma entidade de serviço no locatário correspondente do Microsoft Entra associado à assinatura que contém o recurso. Assim, no contexto do Service Fabric, há suporte para identidades gerenciadas apenas em aplicativos implantados como recursos do Azure.

Importante

Antes de usar a identidade gerenciada de um aplicativo do Service Fabric, o aplicativo cliente precisa receber acesso ao recurso protegido. Confira a lista de serviços do Azure que dão suporte à autenticação do Microsoft Entra para verificar se há suporte e, em seguida, a documentação do respectivo serviço para obter etapas específicas para conceder acesso de identidade aos recursos de interesse.

Aproveitar uma identidade gerenciada usando o Azure.Identity

O SDK de Identidade do Azure agora dá suporte ao Service Fabric. Usando o Azure.Identity, é fácil escrever um código que use identidades gerenciadas de aplicativo do Service Fabric, pois ele processa a busca de tokens, o armazenamento de tokens em cache e a autenticação do servidor. Durante o acesso à maioria dos recursos do Azure, o conceito de um token fica oculto.

O suporte ao Service Fabric está disponível nas seguintes versões para estes idiomas:

Exemplo do C# de inicialização de credenciais e uso das credenciais para buscar um segredo do Azure Key Vault:

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

namespace MyMIService
{
    internal sealed class MyMIService : StatelessService
    {
        protected override async Task RunAsync(CancellationToken cancellationToken)
        {
            try
            {
                // Load the service fabric application managed identity assigned to the service
                ManagedIdentityCredential creds = new ManagedIdentityCredential();

                // Create a client to keyvault using that identity
                SecretClient client = new SecretClient(new Uri("https://mykv.vault.azure.net/"), creds);

                // Fetch a secret
                KeyVaultSecret secret = (await client.GetSecretAsync("mysecret", cancellationToken: cancellationToken)).Value;
            }
            catch (CredentialUnavailableException e)
            {
                // Handle errors with loading the Managed Identity
            }
            catch (RequestFailedException)
            {
                // Handle errors with fetching the secret
            }
            catch (Exception e)
            {
                // Handle generic errors
            }
        }
    }
}

Como adquirir um token de acesso usando a API REST

Nos clusters habilitados para identidade gerenciada, o runtime do Service Fabric expõe um ponto de extremidade do localhost que os aplicativos podem usar para obter tokens de acesso. O ponto de extremidade fica disponível em cada nó do cluster e acessível a todas as entidades nesse nó. Os chamadores autorizados podem obter tokens de acesso chamando esse ponto de extremidade e apresentando um código de autenticação. O código é gerado pelo runtime do Service Fabric para cada ativação de pacote de código de serviço distinto e é vinculado ao tempo de vida do processo que hospeda esse pacote de código de serviço.

Especificamente, o ambiente de um serviço do Service Fabric habilitado para identidade gerenciada será propagado com as seguintes variáveis:

  • 'IDENTITY_ENDPOINT': o ponto de extremidade do localhost correspondente à identidade gerenciada do serviço
  • 'IDENTITY_HEADER': um código de autenticação exclusivo que representa o serviço no nó atual
  • 'IDENTITY_SERVER_THUMBPRINT': impressão digital do servidor de identidade gerenciada do Service Fabric

Importante

O código do aplicativo deve considerar o valor da variável de ambiente 'IDENTITY_HEADER' como dados confidenciais. Esse valor não deve ser registrado em log nem disseminado de outras formas. O código de autenticação não tem valor fora do nó local nem depois que o processo que hospeda o serviço termina, mas ele representa a identidade do serviço do Service Fabric e, portanto, deve ser tratado com as mesmas precauções que o token de acesso em si.

Para obter um token, o cliente executa as seguintes etapas:

  • forma um URI concatenando o ponto de extremidade de identidade gerenciada (valor de IDENTITY_ENDPOINT) com a versão da API e o recurso (público-alvo) necessário para o token
  • cria uma solicitação HTTP GET para o URI especificado
  • adiciona a lógica de validação de certificado do servidor apropriada
  • adiciona o código de autenticação (valor de IDENTITY_HEADER) como um cabeçalho à solicitação
  • envia a solicitação

Uma resposta bem-sucedida conterá um conteúdo JSON representando o token de acesso resultante, bem como os metadados que o descrevem. Uma resposta com falha também incluirá uma explicação da falha. Veja abaixo detalhes adicionais sobre o tratamento de erro.

Os tokens de acesso são armazenados em cache pelo Service Fabric em vários níveis (nó, cluster, serviço do provedor de recursos), portanto, uma resposta bem-sucedida não significa, necessariamente, que o token foi emitido diretamente em resposta à solicitação do aplicativo do usuário. Os tokens são armazenados em cache por um período inferior aos respectivos tempos de vida, portanto, os aplicativos têm a garantia de receber um token válido. É recomendado que o código do aplicativo armazene em cache todos os tokens de acesso adquiridos. A chave do cache deve incluir o público-alvo (uma derivação dele).

Solicitação de exemplo:

GET 'https://localhost:2377/metadata/identity/oauth2/token?api-version=2019-07-01-preview&resource=https://vault.azure.net/' HTTP/1.1 Secret: 912e4af7-77ba-4fa5-a737-56c8e3ace132

em que:

Elemento Descrição
GET O verbo HTTP, indicando que você deseja recuperar os dados do ponto de extremidade. Neste caso, um token de acesso OAuth.
https://localhost:2377/metadata/identity/oauth2/token O ponto de extremidade de identidade gerenciada dos aplicativos do Service Fabric, fornecido por meio da variável de ambiente IDENTITY_ENDPOINT.
api-version Um parâmetro de cadeia de caracteres de consulta, especificando a versão da API do serviço de token de identidade gerenciada. No momento, o único valor aceito é 2019-07-01-preview e está sujeito a alterações.
resource Um parâmetro de cadeia de caracteres de consulta que indica o URI da ID do aplicativo do recurso de destino. Isso será refletido como a declaração aud (público-alvo) do token emitido. Este exemplo solicita um token para acessar o Azure Key Vault, cujo URI da ID do Aplicativo é https://vault.azure.net/.
Secret Um campo de cabeçalho da solicitação HTTP, exigido pelo serviço de token de identidade gerenciada do Service Fabric para que os serviços do Service Fabric autentiquem o chamador. Esse valor é fornecido pelo runtime do SF por meio da variável de ambiente IDENTITY_HEADER.

Exemplo de resposta:

HTTP/1.1 200 OK
Content-Type: application/json
{
    "token_type":  "Bearer",
    "access_token":  "eyJ0eXAiO...",
    "expires_on":  1565244611,
    "resource":  "https://vault.azure.net/"
}

em que:

Elemento Descrição
token_type O tipo de token, nesse caso, um token de acesso de "portador", que significa que o apresentador ('portador') desse token é a entidade pretendida do token.
access_token O token de acesso solicitado. Ao chamar uma API REST protegida, o token é inserido no campo de cabeçalho de solicitação Authorization como um token "portador", permitindo que a API autentique o chamador.
expires_on O carimbo de data/hora da expiração do token de acesso, representado como o número de segundos de "1970-01-01T0:0:0Z UTC" e corresponde à declaração exp do token. Nesse caso, o token expira em 2019-08-08T06:10:11+00:00 (na RFC 3339)
resource O recurso para o qual o token de acesso foi emitido, especificado por meio do parâmetro de cadeia de caracteres de consulta resource da solicitação, que corresponde à declaração 'aud' do token.

Como adquirir um token de acesso usando C#

Em C#, o código acima se torna:

namespace Azure.ServiceFabric.ManagedIdentity.Samples
{
    using System;
    using System.Net.Http;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using Newtonsoft.Json;

    /// <summary>
    /// Type representing the response of the SF Managed Identity endpoint for token acquisition requests.
    /// </summary>
    [JsonObject]
    public sealed class ManagedIdentityTokenResponse
    {
        [JsonProperty(Required = Required.Always, PropertyName = "token_type")]
        public string TokenType { get; set; }

        [JsonProperty(Required = Required.Always, PropertyName = "access_token")]
        public string AccessToken { get; set; }

        [JsonProperty(PropertyName = "expires_on")]
        public string ExpiresOn { get; set; }

        [JsonProperty(PropertyName = "resource")]
        public string Resource { get; set; }
    }

    /// <summary>
    /// Sample class demonstrating access token acquisition using Managed Identity.
    /// </summary>
    public sealed class AccessTokenAcquirer
    {
        /// <summary>
        /// Acquire an access token.
        /// </summary>
        /// <returns>Access token</returns>
        public static async Task<string> AcquireAccessTokenAsync()
        {
            var managedIdentityEndpoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT");
            var managedIdentityAuthenticationCode = Environment.GetEnvironmentVariable("IDENTITY_HEADER");
            var managedIdentityServerThumbprint = Environment.GetEnvironmentVariable("IDENTITY_SERVER_THUMBPRINT");
            // Latest api version, 2019-07-01-preview is still supported.
            var managedIdentityApiVersion = Environment.GetEnvironmentVariable("IDENTITY_API_VERSION");
            var managedIdentityAuthenticationHeader = "secret";
            var resource = "https://management.azure.com/";

            var requestUri = $"{managedIdentityEndpoint}?api-version={managedIdentityApiVersion}&resource={HttpUtility.UrlEncode(resource)}";

            var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
            requestMessage.Headers.Add(managedIdentityAuthenticationHeader, managedIdentityAuthenticationCode);
            
            var handler = new HttpClientHandler();
            handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, certChain, policyErrors) =>
            {
                // Do any additional validation here
                if (policyErrors == SslPolicyErrors.None)
                {
                    return true;
                }
                return 0 == string.Compare(cert.GetCertHashString(), managedIdentityServerThumbprint, StringComparison.OrdinalIgnoreCase);
            };

            try
            {
                var response = await new HttpClient(handler).SendAsync(requestMessage)
                    .ConfigureAwait(false);

                response.EnsureSuccessStatusCode();

                var tokenResponseString = await response.Content.ReadAsStringAsync()
                    .ConfigureAwait(false);

                var tokenResponseObject = JsonConvert.DeserializeObject<ManagedIdentityTokenResponse>(tokenResponseString);

                return tokenResponseObject.AccessToken;
            }
            catch (Exception ex)
            {
                string errorText = String.Format("{0} \n\n{1}", ex.Message, ex.InnerException != null ? ex.InnerException.Message : "Acquire token failed");

                Console.WriteLine(errorText);
            }

            return String.Empty;
        }
    } // class AccessTokenAcquirer
} // namespace Azure.ServiceFabric.ManagedIdentity.Samples

Como acessar o cofre de chaves de um aplicativo do Service Fabric usando a identidade gerenciada

Este exemplo se baseia no código acima para demonstrar o acesso a um segredo armazenado em um cofre de chaves usando a identidade gerenciada.

        /// <summary>
        /// Probe the specified secret, displaying metadata on success.  
        /// </summary>
        /// <param name="vault">vault name</param>
        /// <param name="secret">secret name</param>
        /// <param name="version">secret version id</param>
        /// <returns></returns>
        public async Task<string> ProbeSecretAsync(string vault, string secret, string version)
        {
            // initialize a KeyVault client with a managed identity-based authentication callback
            var kvClient = new Microsoft.Azure.KeyVault.KeyVaultClient(new Microsoft.Azure.KeyVault.KeyVaultClient.AuthenticationCallback((a, r, s) => { return AuthenticationCallbackAsync(a, r, s); }));

            Log(LogLevel.Info, $"\nRunning with configuration: \n\tobserved vault: {config.VaultName}\n\tobserved secret: {config.SecretName}\n\tMI endpoint: {config.ManagedIdentityEndpoint}\n\tMI auth code: {config.ManagedIdentityAuthenticationCode}\n\tMI auth header: {config.ManagedIdentityAuthenticationHeader}");
            string response = String.Empty;

            Log(LogLevel.Info, "\n== {DateTime.UtcNow.ToString()}: Probing secret...");
            try
            {
                var secretResponse = await kvClient.GetSecretWithHttpMessagesAsync(vault, secret, version)
                    .ConfigureAwait(false);

                if (secretResponse.Response.IsSuccessStatusCode)
                {
                    // use the secret: secretValue.Body.Value;
                    response = String.Format($"Successfully probed secret '{secret}' in vault '{vault}': {PrintSecretBundleMetadata(secretResponse.Body)}");
                }
                else
                {
                    response = String.Format($"Non-critical error encountered retrieving secret '{secret}' in vault '{vault}': {secretResponse.Response.ReasonPhrase} ({secretResponse.Response.StatusCode})");
                }
            }
            catch (Microsoft.Rest.ValidationException ve)
            {
                response = String.Format($"encountered REST validation exception 0x{ve.HResult.ToString("X")} trying to access '{secret}' in vault '{vault}' from {ve.Source}: {ve.Message}");
            }
            catch (KeyVaultErrorException kvee)
            {
                response = String.Format($"encountered KeyVault exception 0x{kvee.HResult.ToString("X")} trying to access '{secret}' in vault '{vault}': {kvee.Response.ReasonPhrase} ({kvee.Response.StatusCode})");
            }
            catch (Exception ex)
            {
                // handle generic errors here
                response = String.Format($"encountered exception 0x{ex.HResult.ToString("X")} trying to access '{secret}' in vault '{vault}': {ex.Message}");
            }

            Log(LogLevel.Info, response);

            return response;
        }

        /// <summary>
        /// KV authentication callback, using the application's managed identity.
        /// </summary>
        /// <param name="authority">The expected issuer of the access token, from the KV authorization challenge.</param>
        /// <param name="resource">The expected audience of the access token, from the KV authorization challenge.</param>
        /// <param name="scope">The expected scope of the access token; not currently used.</param>
        /// <returns>Access token</returns>
        public async Task<string> AuthenticationCallbackAsync(string authority, string resource, string scope)
        {
            Log(LogLevel.Verbose, $"authentication callback invoked with: auth: {authority}, resource: {resource}, scope: {scope}");
            var encodedResource = HttpUtility.UrlEncode(resource);

            // This sample does not illustrate the caching of the access token, which the user application is expected to do.
            // For a given service, the caching key should be the (encoded) resource uri. The token should be cached for a period
            // of time at most equal to its remaining validity. The 'expires_on' field of the token response object represents
            // the number of seconds from Unix time when the token will expire. You may cache the token if it will be valid for at
            // least another short interval (1-10s). If its expiration will occur shortly, don't cache but still return it to the 
            // caller. The MI endpoint will not return an expired token.
            // Sample caching code:
            //
            // ManagedIdentityTokenResponse tokenResponse;
            // if (responseCache.TryGetCachedItem(encodedResource, out tokenResponse))
            // {
            //     Log(LogLevel.Verbose, $"cache hit for key '{encodedResource}'");
            //
            //     return tokenResponse.AccessToken;
            // }
            //
            // Log(LogLevel.Verbose, $"cache miss for key '{encodedResource}'");
            //
            // where the response cache is left as an exercise for the reader. MemoryCache is a good option, albeit not yet available on .net core.

            var requestUri = $"{config.ManagedIdentityEndpoint}?api-version={config.ManagedIdentityApiVersion}&resource={encodedResource}";
            Log(LogLevel.Verbose, $"request uri: {requestUri}");

            var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
            requestMessage.Headers.Add(config.ManagedIdentityAuthenticationHeader, config.ManagedIdentityAuthenticationCode);
            Log(LogLevel.Verbose, $"added header '{config.ManagedIdentityAuthenticationHeader}': '{config.ManagedIdentityAuthenticationCode}'");

            var response = await httpClient.SendAsync(requestMessage)
                .ConfigureAwait(false);
            Log(LogLevel.Verbose, $"response status: success: {response.IsSuccessStatusCode}, status: {response.StatusCode}");

            response.EnsureSuccessStatusCode();

            var tokenResponseString = await response.Content.ReadAsStringAsync()
                .ConfigureAwait(false);

            var tokenResponse = JsonConvert.DeserializeObject<ManagedIdentityTokenResponse>(tokenResponseString);
            Log(LogLevel.Verbose, "deserialized token response; returning access code..");

            // Sample caching code (continuation):
            // var expiration = DateTimeOffset.FromUnixTimeSeconds(Int32.Parse(tokenResponse.ExpiresOn));
            // if (expiration > DateTimeOffset.UtcNow.AddSeconds(5.0))
            //    responseCache.AddOrUpdate(encodedResource, tokenResponse, expiration);

            return tokenResponse.AccessToken;
        }

        private string PrintSecretBundleMetadata(SecretBundle bundle)
        {
            StringBuilder strBuilder = new StringBuilder();

            strBuilder.AppendFormat($"\n\tid: {bundle.Id}\n");
            strBuilder.AppendFormat($"\tcontent type: {bundle.ContentType}\n");
            strBuilder.AppendFormat($"\tmanaged: {bundle.Managed}\n");
            strBuilder.AppendFormat($"\tattributes:\n");
            strBuilder.AppendFormat($"\t\tenabled: {bundle.Attributes.Enabled}\n");
            strBuilder.AppendFormat($"\t\tnbf: {bundle.Attributes.NotBefore}\n");
            strBuilder.AppendFormat($"\t\texp: {bundle.Attributes.Expires}\n");
            strBuilder.AppendFormat($"\t\tcreated: {bundle.Attributes.Created}\n");
            strBuilder.AppendFormat($"\t\tupdated: {bundle.Attributes.Updated}\n");
            strBuilder.AppendFormat($"\t\trecoveryLevel: {bundle.Attributes.RecoveryLevel}\n");

            return strBuilder.ToString();
        }

        private enum LogLevel
        {
            Info,
            Verbose
        };

        private void Log(LogLevel level, string message)
        {
            if (level != LogLevel.Verbose
                || config.DoVerboseLogging)
            {
                Console.WriteLine(message);
            }
        }

Tratamento de erros

O campo 'código de status' do cabeçalho de resposta HTTP indica o status de êxito da solicitação. O status '200 OK' indica êxito e a resposta incluirá o token de acesso, conforme a descrição acima. Veja a seguir uma breve enumeração das possíveis respostas de erro.

Código de status Motivo do erro Como tratar
404 Não Encontrado: Código de autenticação desconhecido ou o aplicativo não recebeu uma identidade gerenciada. Corrigir a configuração do aplicativo ou o código de aquisição do token.
429 Número excessivo de solicitações. Limite atingido, imposto pelo Microsoft Entra ID ou pelo SF. Tentar novamente com Retirada Exponencial. Consulte as diretrizes abaixo.
o erro 4xx na solicitação. Um ou mais parâmetros de solicitação estava incorreto. Não tente novamente. Examine os detalhes do erro para obter mais informações. Os erros 4xx são erros de tempo de design.
Erro 5xx do serviço. O subsistema de identidade gerenciada ou o Microsoft Entra ID retornaram um erro transitório. É seguro tentar novamente após um curto período. Pode ocorrer uma condição de limitação (429) na nova tentativa.

Se ocorrer um erro, o corpo da resposta HTTP correspondente conterá um objeto JSON com os detalhes do erro:

Elemento Descrição
code Código do erro.
correlationId Uma ID de correlação que pode ser usada para a depuração.
message Descrição detalhada do erro. Descrições de erro podem ser alteradas a qualquer momento. Não dependa da mensagem de erro em si.

Exemplo de erro:

{"error":{"correlationId":"7f30f4d3-0f3a-41e0-a417-527f21b3848f","code":"SecretHeaderNotFound","message":"Secret is not found in the request headers."}}

Veja uma lista de erros típicos do Service Fabric específicos de identidades gerenciadas:

Código Mensagem Descrição
SecretHeaderNotFound O segredo não foi encontrado nos cabeçalhos da solicitação. O código de autenticação não foi fornecido com a solicitação.
ManagedIdentityNotFound Identidade gerenciada não encontrada para o host de aplicativo especificado. O aplicativo não tem nenhuma identidade ou o código de autenticação é desconhecido.
ArgumentNullOrEmpty O parâmetro 'resource' não deve ser uma cadeia de caracteres nula ou vazia. O recurso (público-alvo) não foi fornecido na solicitação.
InvalidApiVersion Não há suporte para a versão da API ''. A versão com suporte é a '2019-07-01-preview'. Versão da API ausente ou sem suporte especificada no URI de solicitação.
InternalServerError Ocorreu um erro. Foi encontrado um erro no subsistema de identidade gerenciada, possivelmente fora da pilha do Service Fabric. A causa mais provável é um valor incorreto especificado para o recurso (verifique se há '/'? à direita)

Repita a orientação

Geralmente, o único código de erro que aceita novas tentativas é o 429 (Muitas Solicitações). Os erros internos do servidor ou os códigos de erro 5xx podem ser tentados novamente, embora a causa possa ser permanente.

As limitações se aplicam ao número de chamadas feitas ao subsistema de identidade gerenciada. Especificamente as dependências de "upstream" (o serviço do Azure de identidade gerenciada ou o serviço de token seguro). O Service Fabric armazena tokens em cache em vários níveis no pipeline, mas, considerando a natureza distribuída dos componentes envolvidos, o chamador pode experimentar respostas de limitação inconsistentes (ou seja, limitado em um nó/instância de um aplicativo, mas não em um nó diferente ao solicitar um token para a mesma identidade). Quando a condição de limitação é definida, as próximas solicitações do mesmo aplicativo podem falhar com o código de status HTTP 429 (Muitas Solicitações) até que a condição seja eliminada.

É recomendado que as solicitações que falharam devido à limitação sejam tentadas novamente com uma retirada exponencial, da seguinte maneira:

Índice de chamadas Ação ao receber o 429
1 Esperar um segundo e tentar novamente
2 Esperar dois segundos e tentar novamente
3 Esperar quatro segundos e tentar novamente
4 Esperar oito segundos e tentar novamente
4 Esperar oito segundos e tentar novamente
5 Esperar 16 segundos e tentar novamente

IDs de recurso para serviços do Azure

Consulte Serviços do Azure que dão suporte à autenticação do Microsoft Entra para obter uma lista de recursos que dão suporte ao Microsoft Entra ID e as respetivas IDs dos recursos.

Próximas etapas