Requisição de Web API com Certificado

Bom dia,

Estamos desenvolvendo uma página que precisará fazer uma requisição a uma API que demanda o envio de um certificado digital a cada chamada. Qual a forma ideal para eu subir esse certificado e como fazer para acessar seu caminho por dentro de um form do latromi?

Eu fiz um código c# local na minha máquina mas estou com dificuldades de transcrevê-lo para o latromi, seria mais ou menos isso:

using System;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Newtonsoft.Json;

public class TokenResponse
{
    public string access_token { get; set; }
}

public class ProdAuthentication
{
    public string GetToken(string clientId, string clientSecret)
    {
        string tokenEndpoint = "ENDPOINT_API";
        string grantType = "client_credentials";
        string certificatePath = "PATH_CERTIFICADO";
        string certificatePassword = "SENHA_CERTIFICADO";

        try
        {
            X509Certificate2 certificate = new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.DefaultKeySet);

            ServicePointManager.Expect100Continue = true;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(tokenEndpoint);
            request.ContentType = "application/x-www-form-urlencoded";
            request.Method = "POST";
            request.ClientCertificates.Add(certificate);

            var data = Encoding.ASCII.GetBytes($"grant_type={Uri.EscapeDataString(grantType)}&client_id={Uri.EscapeDataString(clientId)}&client_secret={Uri.EscapeDataString(clientSecret)}");
            request.ContentLength = data.Length;

            using (Stream stream = request.GetRequestStream())
            {
                stream.Write(data, 0, data.Length);
            }

            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    using (StreamReader reader = new StreamReader(responseStream))
                    {
                        string jsonResponse = reader.ReadToEnd();
                        TokenResponse tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(jsonResponse);
                        return tokenResponse.access_token;
                    }
                }
            }
        }
        catch (WebException webEx)
        {
            string responseText = string.Empty;
            if (webEx.Response != null)
            {
                using (var streamReader = new StreamReader(webEx.Response.GetResponseStream()))
                {
                    responseText = streamReader.ReadToEnd();
                }
            }

            throw new Exception("Erro ao solicitar o token.\r\nDetalhe: " + webEx.Message + "\r\nResposta: " + responseText);
        }
        catch (Exception ex)
        {
            throw new Exception("Erro ao solicitar o token.\r\nDetalhe: " + ex.Message);
        }
    }
}


class Program
{
    static void Main(string[] args)
    {
        // Substitua os valores de clientId e clientSecret pelos valores corretos
        string clientId = "CLIENT_ID";
        string clientSecret = "CLIENT_SECRET";

        try
        {
            ProdAuthentication prodAuth = new ProdAuthentication();
            string tokenResponse = prodAuth.GetToken(clientId, clientSecret);
            Console.WriteLine("Token obtido com sucesso:");
            Console.WriteLine(tokenResponse);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Erro ao obter o token:");
            Console.WriteLine(ex.Message);
        }
    }
}
2 curtidas

Olá @Rafael!

Para adaptar o código, você precisa considerar que o Latromi espera um código que possa ser executado dentro de um método. É como um “Bloco anônimo” de código C#.

Então primeiramente, precisamos remover a declaração de classes e de métodos, deixando apenas o código que será executado.

Ao invés de receber os dados dos argumentos do método C#, precisamos modificar para buscar de Variáveis, Campos ou Argumentos (do Procedimento). E quanto ao resultado, precisamos devolver o valor para uma Variável ou Campo do Formulário.

Segue abaixo o código adaptado:

using System;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Newtonsoft.Json;

// Recebe "clientId" e "secret" via parâmetros do procedimento (argumentos);
string clientId = (string)Arguments["clientId"].Value;
string clientSecret = (string)Arguments["secret"].Value;

string tokenEndpoint = "ENDPOINT_API";
string grantType = "client_credentials";
string certificatePath = "PATH_CERTIFICADO";
string certificatePassword = "SENHA_CERTIFICADO";

// Limpa variável do Form que armazena o "access_token"
Variables["v_access_token"].Value = "";

try
{
    X509Certificate2 certificate = new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.DefaultKeySet);

    ServicePointManager.Expect100Continue = true;
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(tokenEndpoint);
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "POST";
    request.ClientCertificates.Add(certificate);

    var data = Encoding.ASCII.GetBytes($"grant_type={Uri.EscapeDataString(grantType)}&client_id={Uri.EscapeDataString(clientId)}&client_secret={Uri.EscapeDataString(clientSecret)}");
    request.ContentLength = data.Length;

    using (Stream stream = request.GetRequestStream())
    {
        stream.Write(data, 0, data.Length);
    }

    using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
    using (Stream responseStream = response.GetResponseStream())
    using (StreamReader reader = new StreamReader(responseStream))
    {
        string jsonResponse = reader.ReadToEnd();
        var tokenResponse = JsonConvert.DeserializeObject<dynamic>(jsonResponse);

        // Preenche variável do Formulário com o "access_token"
        Variables["v_access_token"].Value = tokenResponse.access_token;
    }
}
catch (WebException webEx)
{
    string responseText = string.Empty;
    if (webEx.Response != null)
    {
        using (var streamReader = new StreamReader(webEx.Response.GetResponseStream()))
        {
            responseText = streamReader.ReadToEnd();
        }
    }

    throw new Exception("Erro ao solicitar o token.\r\nDetalhe: " + webEx.Message + "\r\nResposta: " + responseText);
}
catch (Exception ex)
{
    throw new Exception("Erro ao solicitar o token.\r\nDetalhe: " + ex.Message);
}
A seção de declações "using" não é aceita dentro de um método, mas o Latromi trata este bloco de uma maneira especial.

Note que estou recebendo os valores de “clientId” e “secret” da propriedade “Arguments” (argumentos do Procedimento):

Note também que o resultado está sendo retornado para a variável do Formulário “v_access_token”:

 // Preenche variável do Formulário com o "access_token"
 Variables["v_access_token"].Value = tokenResponse.access_token;

Agora basta chamar este procedimento a partir de outro, como por exemplo, a partir do Procedimento associado ao Click de um botão. Para chamar um procedimento, use a ação “Chamar Procedimento”.

Para finalizar, vou deixar o link de alguns tópicos relacionados ao assunto:

Boa tarde Daniel,

Obrigado pelo retorno. Mas e quanto a questao do certificado? Essa API que vou consumir demanda o envio do certificado a cada requisição, tanto na obtenção do token de acesso como depois no consumo das funções específicas. Como testei localmente no meu ambiente, indicar um path onde o certificado está e depois atribuir com:

X509Certificate2 certificate = new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.DefaultKeySet);

é tranquilo. Qual a forma ideal pra eu conseguir fazer isso via latromi?

Basta informar na variável “certificatePath” o caminho do certificado no servidor onde o site está hospedado.

E como eu vejo essa estrutura de pastas/diretórios? Só costumo upar arquivos aqui:

Mas de qualquer forma nao tem o path raiz

Você precisa acessar o servidor (Windows Server) através de um acesso remoto, e colocar o certificado em uma pasta. Se você não tiver acesso, peça para o responsável pela sua infraestrutura colocar o certificado em uma pasta para você.

Bom dia Daniel,

Consegui subir o arquivo do certificado digital para o servidor. Tentei adaptar o código para o padrão que tu forneceu no tópico:

Obter um Token OAuth 2.0 com C#

Cheguei nisso adicionando a parte do certificado:

using System;
using System.IO;
using System.Reflection;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Newtonsoft.Json;

string tokenEndpoint = "XXXXX";
string clientId = "XXXXX";
string clientSecret = "XXXXX";
string grantType = "client_credentials";
string certificatePath = @"XXXXX";
string certificatePassword = "XXXXX";

// Carregue o certificado digital
X509Certificate2 certificate = new X509Certificate2(certificatePath, certificatePassword);

// Configurar o HttpClientHandler com o certificado
HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);

using (HttpClient client = new HttpClient(handler))
{
    // Crie os parâmetros do form-data
    var formData = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("grant_type", grantType),
        new KeyValuePair<string, string>("client_id", clientId),
        new KeyValuePair<string, string>("client_secret", clientSecret),
    }); 

    // Faça a solicitação POST para o endpoint do tokena
    var response = client.PostAsync(tokenEndpoint, formData).Result;

    // Garanta que a solicitação foi bem-sucedida
    response.EnsureSuccessStatusCode();

    // Leia o resultado da requisição
    var result = response.Content.ReadAsStringAsync().Result;
    
    // Deserialize o JSON usando a biblioteca NewtonSoft usando "dynamic"
    var tokenResult = JsonConvert.DeserializeObject<dynamic>(result);

    // Preencha a variável com o valor da propriedade "access_token" do JSON.
    Variables["token"].Value = (string)tokenResult.access_token;
}

Quando realizo a chamado estou tendo o seguinte erro:

image

Removendo a parte do handler do certificado, a requisição “funciona” retornando o status 403 de forbidden:

image

Eu testei o path no servidor onde o certificado está e consigo listar os arquivos da pasta, então imagino que não seja um problema de acesso.

O código funciona localmente na minha máquina então pensei que pudesse ser alguma especificidade do latromi no tratamento desse handler.

Consegue me auxiliar com alguma indicação?

A “Exception” original está “escondida” dentro do AggregateException.

Tenta trocar o .Result por .GetAwaiter().GetResult(). Isso deve fazer com que a Exception original seja exibida:

var result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();

O erro ta sendo ja na chamada, alterei para:

 var response = client.PostAsync(tokenEndpoint, formData).GetAwaiter().GetResult();

Porém a mensagem de erro recebida é muito genérica:

image

Tenta envolver o seu código em blobo try/catch para tentar extrair informações mais detalhadas:

using System;
using System.IO;
using System.Reflection;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Newtonsoft.Json;

try
{
    ...
    ...
    ...
}
catch(Exception ex)
{
    throw new InvalidOperationException(ex.ToString());
}

Esta é a mensagem de erro obtida:

Uma exceção foi acionada pelo destino de uma chamada. Erro ao executar o código C# dinâmico. System.Net.Http.HttpRequestException: Ocorreu um erro ao enviar a solicitação. 
---> System.Net.WebException: A solicitação foi anulada: Não foi possível criar um canal seguro para SSL/TLS.
em System.Net.HttpWebRequest.EndGetRequestStream(IAsyncResult asyncResult, TransportContext& context)
em System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
--- Fim do rastreamento de pilha de exceções internas ---
em System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
em System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
em LATROMI.DynamicLib.FRM00001128100049520240506160231880305.Main.M3A614E8D85F64AB7873DB27127B772C2()

Essa mensagem geralmente indica que o servidor de onde está partindo a requisição não tem os pacotes de cripografia necessários para estabelecer a comunicação com o servidor destino.

Mais informações neste tópico: Não foi possível criar um canal seguro para SSL/TLS

Qual versão do Windows Server é usada no servidor?

Essa é a configuração do servidor:

E esses são os protocolos suportados pela endereço destino obtidos no link do tópico mencionado:

@Rafael, você precisa comparar os recursos de criptografia do seu servidor com o servidor que você está tentando conectar.

No tópico que recomendei, tem um site chamado SSL Server Test. Lá você coloca o domínio que quer acessar, e ele te devolve os recursos de criptografia que estão disponíveis (só aceitam conexões de servidores que atendam aqueles requisitos).

Você precisa verificar no servidor de origem (onde o Latromi está instalado) tem os recursos necessários para estabeler a conexão.

Você tentou o executar o IIS Crypto?

Bom dia Daniel,

A questão é que eu não tenho acesso ao servidor onde o latromi está, é o pessoal da praxio que gerencia, então eu preciso indicar pra eles o que deve ser feito.

Pelo que me passaram tanto SSL como TLS estão ativos:

image

E o servidor destino usa esses que estão habilitados pelo que entendi:

Bom dia @Rafael!

Não são apenas os protocolos, mas os Conjuntos de Criptografia (Cipher Suits) também precisam ser levados em consideração.

Nessa página da Microsoft, você encontra a relação de todos os Conjuntos de Criptografias disponíveis no Windows Server 2019.

Você precisa verificar se pelo menos um Conjunto de Criptografia (Cipher Suit) habilitado no servidor destino está disponível no Windows Server 2019. Se encontrar algum, ele pode estar apenas desabilitado, e pode ser habilitado pelo IIS Crypto.

Bom dia Daniel,

esse é o host do servidor destino: sts.itau.com.br, que pode utilizar essas cipher suites:

Me retornaram que todas as cipher suites já estavam habilitadas no windows server do servidor do latromi:

Mas o problema segue, o que mais eu poderia fazer pra detectar o problema?

Olá @Rafael!

Pela configuração, parece que os conjuntos de criptografia estão de acordo nas duas pontas.

Existem duas coisas que ainda podem ser tentadas:

  • Usar a opção Best Practices do IIS Crypto, e reiniciar o servidor.
  • Usar um usuário com perfil de administrador para executar o Pool de Aplicativos do IIS.

Se não resolver, crie um ticket de atendimento em https://latromi.freshdesk.com/, informando todas as informações necessárias para reproduzir essa requisição de Web API.

Olá @Rafael!

Após realizar algumas pesquisas e também alguns testes, descobri que o problema pode estar nos privilégios associados a Identidade usada no Pool de Aplicativos. Por padrão, o Pool é criado com a Identidade “ApplicationPoolIdentity”, que é a que possui menos privilégios do que as outras.

Tente usar “NetworkService” ou “LocalSystem”:

Neste link tem explicações sobre cada Identidade:

2 curtidas

Bom dia Daniel,

Era isso mesmo, ajustamos o servidor e funcionou.

Muito obrigado pelo apoio nesse processo todo, exigiu bastante coisa!

2 curtidas