Настройка взаимной проверки подлинности для Службы приложений Azure

Доступ к службе приложений Azure можно ограничить, используя разные способы проверки подлинности. Один из способов сделать это — запросить сертификат клиента, когда запрос клиента передается по протоколу TLS/SSL, и проверить сертификат. Этот механизм известен как взаимная аутентификация TLS или аутентификация по сертификату клиента. В этой статье описано, как настроить ваше приложение для использования проверки подлинности сертификата клиента.

Примечание

Если для доступа к сайту используется протокол HTTP, а не HTTPS, вы не получите сертификат клиента. Поэтому, если ваше приложение требует клиентские сертификаты, не следует разрешать запросы к приложению по протоколу HTTP.

Подготовка веб-приложения

Чтобы создать пользовательские привязки TLS/SSL или разрешить сертификаты клиента для приложения Службы приложений, вам нужен план службы приложений одной из следующих ценовых категорий: Базовый, Стандартный, Премиум или Изолированный. Чтобы убедиться, что ваше веб-приложение относится к поддерживаемой ценовой категории, выполните следующие действия:

Перейдите к своему веб-приложению

  1. В поле поиска портала Azure найдите и выберите Службы приложений.

    Снимок экрана: портал Azure, поле поиска и выбранные

  2. На странице Службы приложений выберите имя веб-приложения.

    Снимок экрана: страница

    Теперь вы находитесь на странице управления веб-приложением.

Проверка ценовой категории

  1. В левом меню для веб-приложения в разделе Настройки выберите Вертикально увеличить масштаб (план службы приложений).

    Снимок экрана: меню веб-приложения, раздел

  2. Убедитесь, что ваше веб-приложение не относится к уровню F1 или D1, который не поддерживает пользовательский протокол TLS/SSL.

  3. Если вам нужно перевести приложение в другую ценовую категорию, выполните действия в следующем разделе. В противном случае закройте страницу вертикального увеличения масштаба и пропустите раздел Изменение уровня плана службы приложений.

Изменение уровня плана службы приложений

  1. Выберите любой оплачиваемый уровень, например B1, B2, B3 или любой другой уровень в категории Рабочая среда.

  2. После выбора области нажмите кнопку Выбрать.

    Когда появится следующее сообщение, операция масштабирования будет завершена.

    Снимок экрана с сообщением о подтверждении операции масштабирования.

Включение сертификатов клиента

Чтобы настроить приложение для запроса сертификатов клиентов, сделайте следующее:

  1. В области навигации слева на странице управления приложением выберите Configuration>General Settings (Конфигурация > Общие параметры).

  2. Задайте для параметра Client certificate mode (Режим сертификата клиента) значение Require (Требуется). Щелкните Сохранить в верхней части страницы.

Чтобы сделать то же самое с помощью Azure CLI, выполните следующую команду в Cloud Shell:

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

Исключение путей из требования проверки подлинности

При включении взаимной проверки подлинности для приложения всем путям в корневом каталоге приложения требуется сертификат клиента для доступа. Чтобы удалить это требование для отдельных путей, определите исключаемые пути как часть конфигурации приложения.

  1. В области навигации слева на странице управления приложением выберите Configuration>General Settings (Конфигурация > Общие параметры).

  2. Рядом с параметром Пути исключения сертификатов щелкните значок редактирования.

  3. Щелкните Создать путь, укажите путь или перечислите пути через , или ;, а затем нажмите кнопку ОК.

  4. Щелкните Сохранить в верхней части страницы.

На следующем снимке экрана любой путь к приложению, который начинается с /public, не запрашивает сертификат клиента. При сопоставлении путей регистр не учитывается.

Пути исключения сертификатов

Доступ к сертификату клиента

В Службе приложений завершение сеанса TLS для запроса происходит в интерфейсной подсистеме балансировки нагрузки. При перенаправлении запроса в код приложения с включенными сертификатами клиентов Служба приложений вставляет заголовок запроса X-ARR-ClientCert с сертификатом клиента. Служба приложений не выполняет никаких действий с этим сертификатом клиента, кроме перенаправления его в приложение. Проверку сертификата клиента выполняет код вашего приложения.

Для ASP.NET сертификат клиента можно получить с помощью свойства HttpRequest.Clientcertificate.

Для других стеков приложений (Node.js, PHP и т. д.) сертификат клиента в вашем приложении доступен через значение заголовка запроса X-ARR-ClientCert в кодировке Base64.

Пример ASP.NET 5+, ASP.NET Core 3.1

В ASP.NET Core для анализа перенаправленных сертификатов предоставляется ПО промежуточного слоя. Для использования перенаправленных заголовков протоколов предоставляется отдельное ПО промежуточного слоя. Для приема перенаправленных сертификатов должны присутствовать оба этих сертификата. Вы можете поместить настраиваемую логику проверки сертификата, как описано в статье Параметры 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?}");
        });
    }
}

Пример веб-форм 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;
            }
        }
    }

Пример на Node.js

В следующем примере кода на Node.js извлекается заголовок X-ARR-ClientCert и используется node-forge для преобразования строки PEM в кодировке Base64 в объект сертификата и для его проверки:

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

Пример Java

Приведенный ниже класс Java кодирует сертификат из X-ARR-ClientCert в экземпляр X509Certificate. certificateIsValid() проверяет, соответствует ли отпечаток сертификата указанному в конструкторе, и не истек ли срок действия сертификата.

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