Configurar autenticação mútua TLS para Serviço de Aplicativo do Azure

Você pode restringir o acesso ao Serviço de Aplicativo do Azure, permitindo diferentes tipos de autenticação para ele. Uma maneira de fazer isso é solicitar um certificado do cliente quando a solicitação do cliente é realizada por TLS/SSL e validar o certificado. Esse mecanismo é chamado de autenticação mútua de TLS ou autenticação de certificado do cliente. Este artigo mostra como configurar o aplicativo para usar a autenticação de certificado do cliente.

Observação

se você acessar seu site por HTTP e não por HTTPS, você não receberá nenhum certificado do cliente. Por isso, se o aplicativo exigir certificados de cliente, você não deverá permitir solicitações ao seu aplicativo via HTTP.

Preparar o aplicativo Web

Para criar associações TLS/SSL personalizadas ou habilitar certificados do cliente para o aplicativo do Serviço de Aplicativo, o Plano do Serviço de Aplicativo precisa estar no nível Básico, Standard, Premium ou Isolado. Para ter certeza de que seu aplicativo Web está no tipo de preço com suporte, siga estas etapas:

Acesse seu aplicativo Web

  1. Na caixa de pesquisa do portal do Azure, localize e selecione Serviços de Aplicativos.

    Screenshot of Azure portal, search box, and

  2. Na página Serviços de Aplicativos, selecione o nome do seu aplicativo Web.

    Screenshot of the App Services page in Azure portal showing a list of all running web apps, with the first app in the list highlighted.

    Agora você está na página de gerenciamento do aplicativo Web.

Verifique o tipo de preço

  1. No menu à esquerda do seu aplicativo Web, na seção Configurações, selecione Aumentar (Plano do Serviço de Aplicativo).

    Screenshot of web app menu,

  2. Verifique se seu aplicativo Web não está na camada F1 ou D1, que não dá suporte a TLS/SSL personalizado.

  3. Se precisar escalar verticalmente, siga as etapas da próxima seção. Caso contrário, feche a página Aumentar e ignore a seção Aumentar seu plano do Serviço de Aplicativo.

Escalar verticalmente seu Plano do Serviço de Aplicativo

  1. Selecione qualquer uma das camadas não gratuitas, como B1, B2, B3 ou qualquer camada na categoria Produção.

  2. Ao concluir, selecione Selecionar.

    Quando a mensagem a seguir for exibida, a operação de escala será concluída.

    Screenshot with confirmation message for scale up operation.

Habilitar certificados do cliente

Para configurar seu aplicativo para exigir certificados do cliente:

  1. No painel de navegação à esquerda da página de gerenciamento do seu aplicativo, selecione Configuração>Configurações Gerais.

  2. Defina o Modo de certificado do cliente como Exigir. Clique em Salvar na parte superior da página.

Para fazer o mesmo com a CLI do Azure, execute o seguinte comando no Cloud Shell:

az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>

Excluir caminhos da exigência de autenticação

Quando você habilita a autenticação mútua para seu aplicativo, todos os caminhos na raiz do aplicativo exigem um certificado do cliente para acesso. Para remover esse requisito para determinados caminhos, defina caminhos de exclusão como parte da configuração do aplicativo.

  1. No painel de navegação à esquerda da página de gerenciamento do seu aplicativo, selecione Configuração>Configurações Gerais.

  2. Ao lado de Certificados de exclusão do cliente, clique no ícone Editar.

  3. Clique em Novo caminho, especifique um caminho ou uma lista de caminhos separados por , ou ; e clique em OK.

  4. Clique em Salvar na parte superior da página.

Na captura de tela a seguir, qualquer caminho para o aplicativo que começa com /public não solicita um certificado do cliente. A correspondência de caminho não diferencia maiúsculas e minúsculas.

Certificate Exclusion Paths

Acessar certificado do cliente

No Serviço de Aplicativo, a terminação TLS da solicitação ocorre no balanceador de carga do front-end. Ao encaminhar a solicitação para o código do aplicativo com certificados do cliente habilitados, o Serviço de Aplicativo injeta um cabeçalho de solicitação X-ARR-ClientCert com o certificado do cliente. O Serviço de Aplicativo não faz nada com esse certificado do cliente além de encaminhá-lo para o aplicativo. O código do aplicativo é responsável por validar o certificado do cliente.

Para ASP.NET, o certificado do cliente está disponível por meio da propriedade HttpRequest.ClientCertificate.

Para outras pilhas de aplicativo (Node.js, PHP etc.), o certificado do cliente estará disponível no seu aplicativo por meio de um valor codificado em Base64 no cabeçalho de solicitação X-ARR-ClientCert.

Exemplo do ASP.NET 5+, ASP.NET Core 3.1

Para o ASP.NET Core, o middleware é fornecido para analisar certificados encaminhados. O middleware separado é fornecido para usar os cabeçalhos de protocolo encaminhados. Ambos precisam estar presentes para que os certificados encaminhados sejam aceitos. Você pode colocar a lógica de validação de certificado personalizada nas opções CertificateAuthentication.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        // Configure the application to use the protocol and client ip address forwared by the frontend load balancer
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            // Only loopback proxies are allowed by default. Clear that restriction to enable this explicit configuration.
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
        });       
        
        // Configure the application to client certificate forwarded the frontend load balancer
        services.AddCertificateForwarding(options => { options.CertificateHeader = "X-ARR-ClientCert"; });

        // Add certificate authentication so when authorization is performed the user will be created from the certificate
        services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        
        app.UseForwardedHeaders();
        app.UseCertificateForwarding();
        app.UseHttpsRedirection();

        app.UseAuthentication()
        app.UseAuthorization();

        app.UseStaticFiles();

        app.UseRouting();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Exemplo de Web Forms do ASP.NET

    using System;
    using System.Collections.Specialized;
    using System.Security.Cryptography.X509Certificates;
    using System.Web;

    namespace ClientCertificateUsageSample
    {
        public partial class Cert : System.Web.UI.Page
        {
            public string certHeader = "";
            public string errorString = "";
            private X509Certificate2 certificate = null;
            public string certThumbprint = "";
            public string certSubject = "";
            public string certIssuer = "";
            public string certSignatureAlg = "";
            public string certIssueDate = "";
            public string certExpiryDate = "";
            public bool isValidCert = false;

            //
            // Read the certificate from the header into an X509Certificate2 object
            // Display properties of the certificate on the page
            //
            protected void Page_Load(object sender, EventArgs e)
            {
                NameValueCollection headers = base.Request.Headers;
                certHeader = headers["X-ARR-ClientCert"];
                if (!String.IsNullOrEmpty(certHeader))
                {
                    try
                    {
                        byte[] clientCertBytes = Convert.FromBase64String(certHeader);
                        certificate = new X509Certificate2(clientCertBytes);
                        certSubject = certificate.Subject;
                        certIssuer = certificate.Issuer;
                        certThumbprint = certificate.Thumbprint;
                        certSignatureAlg = certificate.SignatureAlgorithm.FriendlyName;
                        certIssueDate = certificate.NotBefore.ToShortDateString() + " " + certificate.NotBefore.ToShortTimeString();
                        certExpiryDate = certificate.NotAfter.ToShortDateString() + " " + certificate.NotAfter.ToShortTimeString();
                    }
                    catch (Exception ex)
                    {
                        errorString = ex.ToString();
                    }
                    finally 
                    {
                        isValidCert = IsValidClientCertificate();
                        if (!isValidCert) Response.StatusCode = 403;
                        else Response.StatusCode = 200;
                    }
                }
                else
                {
                    certHeader = "";
                }
            }

            //
            // This is a SAMPLE verification routine. Depending on your application logic and security requirements, 
            // you should modify this method
            //
            private bool IsValidClientCertificate()
            {
                // In this example we will only accept the certificate as a valid certificate if all the conditions below are met:
                // 1. The certificate is not expired and is active for the current time on server.
                // 2. The subject name of the certificate has the common name nildevecc
                // 3. The issuer name of the certificate has the common name nildevecc and organization name Microsoft Corp
                // 4. The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B
                //
                // This example does NOT test that this certificate is chained to a Trusted Root Authority (or revoked) on the server 
                // and it allows for self signed certificates
                //

                if (certificate == null || !String.IsNullOrEmpty(errorString)) return false;

                // 1. Check time validity of certificate
                if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;

                // 2. Check subject name of certificate
                bool foundSubject = false;
                string[] certSubjectData = certificate.Subject.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certSubjectData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundSubject = true;
                        break;
                    }
                }
                if (!foundSubject) return false;

                // 3. Check issuer name of certificate
                bool foundIssuerCN = false, foundIssuerO = false;
                string[] certIssuerData = certificate.Issuer.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certIssuerData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundIssuerCN = true;
                        if (foundIssuerO) break;
                    }

                    if (String.Compare(s.Trim(), "O=Microsoft Corp") == 0)
                    {
                        foundIssuerO = true;
                        if (foundIssuerCN) break;
                    }
                }

                if (!foundIssuerCN || !foundIssuerO) return false;

                // 4. Check thumprint of certificate
                if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), "30757A2E831977D8BD9C8496E4C99AB26CB9622B") != 0) return false;

                return true;
            }
        }
    }

Amostra de Node.js

O seguinte código de exemplo Node.js obtém o cabeçalho X-ARR-ClientCert e usa node-forge para converter a cadeia de caracteres PEM codificada em Base64 em um objeto de certificado e validá-la:

import { NextFunction, Request, Response } from 'express';
import { pki, md, asn1 } from 'node-forge';

export class AuthorizationHandler {
    public static authorizeClientCertificate(req: Request, res: Response, next: NextFunction): void {
        try {
            // Get header
            const header = req.get('X-ARR-ClientCert');
            if (!header) throw new Error('UNAUTHORIZED');

            // Convert from PEM to pki.CERT
            const pem = `-----BEGIN CERTIFICATE-----${header}-----END CERTIFICATE-----`;
            const incomingCert: pki.Certificate = pki.certificateFromPem(pem);

            // Validate certificate thumbprint
            const fingerPrint = md.sha1.create().update(asn1.toDer(pki.certificateToAsn1(incomingCert)).getBytes()).digest().toHex();
            if (fingerPrint.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate time validity
            const currentDate = new Date();
            if (currentDate < incomingCert.validity.notBefore || currentDate > incomingCert.validity.notAfter) throw new Error('UNAUTHORIZED');

            // Validate issuer
            if (incomingCert.issuer.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate subject
            if (incomingCert.subject.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            next();
        } catch (e) {
            if (e instanceof Error && e.message === 'UNAUTHORIZED') {
                res.status(401).send();
            } else {
                next(e);
            }
        }
    }
}

Exemplo de Java

A classe Java a seguir codifica o certificado de X-ARR-ClientCert para uma instância de X509Certificate. certificateIsValid() valida se a impressão digital do certificado corresponde àquela fornecida no construtor e se o certificado não expirou.

import java.io.ByteArrayInputStream;
import java.security.NoSuchAlgorithmException;
import java.security.cert.*;
import java.security.MessageDigest;

import sun.security.provider.X509Factory;

import javax.xml.bind.DatatypeConverter;
import java.util.Base64;
import java.util.Date;

public class ClientCertValidator { 

    private String thumbprint;
    private X509Certificate certificate;

    /**
     * Constructor.
     * @param certificate The certificate from the "X-ARR-ClientCert" HTTP header
     * @param thumbprint The thumbprint to check against
     * @throws CertificateException If the certificate factory cannot be created.
     */
    public ClientCertValidator(String certificate, String thumbprint) throws CertificateException {
        certificate = certificate
                .replaceAll(X509Factory.BEGIN_CERT, "")
                .replaceAll(X509Factory.END_CERT, "");
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        byte [] base64Bytes = Base64.getDecoder().decode(certificate);
        X509Certificate X509cert =  (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(base64Bytes));

        this.setCertificate(X509cert);
        this.setThumbprint(thumbprint);
    }

    /**
     * Check that the certificate's thumbprint matches the one given in the constructor, and that the
     * certificate has not expired.
     * @return True if the certificate's thumbprint matches and has not expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

    /**
     * Check certificate's timestamp.
     * @return Returns true if the certificate has not expired. Returns false if it has expired.
     */
    private boolean certificateHasNotExpired() {
        Date currentTime = new java.util.Date();
        try {
            this.getCertificate().checkValidity(currentTime);
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            return false;
        }
        return true;
    }

    /**
     * Check the certificate's thumbprint matches the given one.
     * @return Returns true if the thumbprints match. False otherwise.
     */
    private boolean thumbprintIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        byte[] der = this.getCertificate().getEncoded();
        md.update(der);
        byte[] digest = md.digest();
        String digestHex = DatatypeConverter.printHexBinary(digest);
        return digestHex.toLowerCase().equals(this.getThumbprint().toLowerCase());
    }

    // Getters and setters

    public void setThumbprint(String thumbprint) {
        this.thumbprint = thumbprint;
    }

    public String getThumbprint() {
        return this.thumbprint;
    }

    public X509Certificate getCertificate() {
        return certificate;
    }

    public void setCertificate(X509Certificate certificate) {
        this.certificate = certificate;
    }
}