Configurar a autenticação mútua de TLS para o Serviço de Aplicações do Azure

Pode restringir o acesso à sua aplicação Serviço de Aplicações do Azure ao ativar diferentes tipos de autenticação para a mesma. Uma forma de o fazer é pedir um certificado de cliente quando o pedido de cliente for superior a TLS/SSL e validar o certificado. Este mecanismo chama-se autenticação mútua TLS ou autenticação de certificado de cliente. Este artigo mostra como configurar a sua aplicação para utilizar a autenticação de certificado de cliente.

Nota

Se aceder ao seu site através de HTTP e não https, não receberá nenhum certificado de cliente. Por isso, se a sua aplicação necessitar de certificados de cliente, não deve permitir pedidos à sua aplicação através de HTTP.

Preparar a sua aplicação Web

Para criar enlaces TLS/SSL personalizados ou ativar certificados de cliente para a sua aplicação Serviço de Aplicações, o seu plano de Serviço de Aplicações tem de estar no escalão Básico, Standard, Premium ou Isolado. Para se certificar de que a sua aplicação Web está no escalão de preço suportado, siga estes passos:

Aceda à sua aplicação Web

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

    Captura de ecrã a mostrar portal do Azure, caixa de pesquisa e

  2. Na página Serviços de Aplicações , selecione o nome da sua aplicação Web.

    Captura de ecrã da página Serviços de Aplicações no portal do Azure a mostrar uma lista de todas as aplicações Web em execução, com a primeira aplicação na lista realçada.

    Está agora na página de gestão da sua aplicação Web.

Verificar o escalão de preço

  1. No menu esquerdo da sua aplicação Web, na secção Definições, selecione Aumentar verticalmente (Serviço de Aplicações plano).

    Captura de ecrã a mostrar o menu da aplicação Web, a secção

  2. Certifique-se de que a sua aplicação Web não está no escalão F1 ou D1 , o que não suporta TLS/SSL personalizado.

  3. Se precisar de aumentar verticalmente, siga os passos na secção seguinte. Caso contrário, feche a página Aumentar verticalmente e ignore a secção Aumentar verticalmente a Serviço de Aplicações plano.

Aumentar verticalmente o seu plano do Serviço de Aplicações

  1. Selecione qualquer escalão não gratuito, como B1, B2, B3 ou qualquer outro escalão na categoria Produção .

  2. Quando terminar, selecione Selecionar.

    Quando a seguinte mensagem for apresentada, a operação de dimensionamento é concluída.

    Captura de ecrã com uma mensagem de confirmação para a operação de aumento vertical.

Ativar certificados de cliente

Para configurar a sua aplicação para exigir certificados de cliente:

  1. No painel de navegação esquerdo da página de gestão da sua aplicação, selecioneDefinições Gerais de Configuração>.

  2. Defina o modo de certificado de cliente como Exigir. Clique em Guardar no início 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 necessidade de autenticação

Quando ativa a autenticação mútua para a sua aplicação, todos os caminhos na raiz da sua aplicação necessitam de um certificado de cliente para acesso. Para remover este requisito para determinados caminhos, defina caminhos de exclusão como parte da configuração da aplicação.

  1. No painel de navegação esquerdo da página de gestão da sua aplicação, selecioneDefinições Gerais de Configuração>.

  2. Junto a Caminhos de exclusão de certificados, 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 Guardar no início da página.

Na captura de ecrã seguinte, qualquer caminho para a sua aplicação que comece por /public não pede um certificado de cliente. A correspondência de caminhos não é sensível a maiúsculas e minúsculas.

Caminhos de Exclusão de Certificados

Certificado de cliente do Access

No Serviço de Aplicações, a terminação TLS do pedido ocorre no balanceador de carga de front-end. Ao reencaminhar o pedido para o código da aplicação com certificados de cliente ativados, Serviço de Aplicações injeta um cabeçalho de X-ARR-ClientCert pedido com o certificado de cliente. Serviço de Aplicações não faz nada com este certificado de cliente a não ser reencaminhar para a sua aplicação. O código da aplicação é responsável pela validação do certificado de cliente.

Para ASP.NET, o certificado de cliente está disponível através da propriedade HttpRequest.ClientCertificate .

Para outras pilhas de aplicações (Node.js, PHP, etc.), o certificado de cliente está disponível na sua aplicação através de um valor codificado base64 no cabeçalho do X-ARR-ClientCert pedido.

ASP.NET 5+, ASP.NET Core exemplo 3.1

Para ASP.NET Core, o middleware é fornecido para analisar certificados reencaminhados. É fornecido middleware separado para utilizar os cabeçalhos de protocolo reencaminhados. Ambos têm de estar presentes para que os certificados reencaminhados sejam aceites. Pode colocar a lógica de validação de certificados 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 ASP.NET WebForms

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

exemplo de Node.js

O seguinte código de exemplo Node.js obtém o X-ARR-ClientCert cabeçalho e utiliza a falsificação de nós para converter a cadeia PEM codificada com base64 num 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 seguinte classe Java codifica o certificado de X-ARR-ClientCert para uma X509Certificate instância. certificateIsValid() valida que o thumbprint do certificado corresponde ao dado no construtor e que 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;
    }
}