Partage via


Configurer l’authentification mutuelle TLS pour Azure App Service

Vous pouvez restreindre l’accès à votre application Azure App Service en activant différents types d’authentification. L’une des façons de procéder consiste à demander un certificat client quand la requête du client est effectuée via TLS/SSL, puis à valider le certificat. Ce mécanisme s’appelle l’authentification mutuelle TLS ou encore l’authentification par certificat client. Cet article montre comment configurer votre application pour utiliser l’authentification par certificat client.

Remarque

Votre code d’application est responsable de la validation du certificat client. App Service ne fait rien d’autre avec ce certificat client que de le transférer à votre application.

si vous accédez à votre site sur HTTP et non HTTPS, vous ne recevez pas de certificat client. Ainsi, si votre application nécessite des certificats clients, vous ne devez pas autoriser les requêtes adressées à votre application via HTTP.

Préparation de votre application web

Si vous souhaitez créer des liaisons TLS/SSL personnalisées ou activer des certificats clients pour votre application App Service, votre plan App Service doit être au niveau De base, Standard, Premium ou Isolé. Pour vous assurer que votre application web se trouve dans le niveau de tarification pris en charge, effectuez les étapes suivantes :

Accédez à votre application web

  1. Dans la zone de recherche du portail Azure, recherchez et sélectionnez App Services.

    Capture d'écran du portail Azure, du champ de recherche et

  2. Dans la page App Services, sélectionnez le nom de votre application web.

    Capture d’écran de la page App Services dans le portail Azure montrant une liste de toutes les applications web en cours d’exécution, avec la première application de la liste mise en évidence.

    Vous êtes maintenant sur la page de gestion de votre application web.

Vérification du niveau tarifaire

  1. Dans le menu de gauche de votre application Web, sous la section Paramètres, sélectionnez Mettre à l'échelle (plan App Service).

    Capture d'écran du menu de l'application Web, section

  2. Assurez-vous que votre application web n’est pas au niveau F1 ou D1, qui ne prend pas en charge le protocole TLS/SSL personnalisé.

  3. Si vous avez besoin de monter en puissance, consultez la section ci-après. Sinon, fermez la page Monter en puissance et ignorez la section Évolution de votre plan App Service.

Évolution de votre plan App Service

  1. Sélectionnez un niveau payant, comme B1, B2, B3, ou n’importe quel niveau dans la catégorie Production.

  2. Quand vous avez terminé, sélectionnez Sélectionner.

    Lorsque le message suivant s’affiche, l’opération de mise à l’échelle est terminée.

    Capture d'écran avec message de confirmation pour l'opération de mise à l'échelle.

Activer les certificats clients

Pour configurer votre application afin d’exiger des certificats clients :

  1. Dans la barre de navigation gauche de la page de gestion de votre application, sélectionnez Configuration>Paramètres généraux.

  2. Sélectionnez le Mode certificat client de votre choix. En haut de la page, sélectionnez Enregistrer.

Modes de certificat client Description
Obligatoire Toutes les requêtes nécessitent un certificat client.
Facultatif Les requêtes peuvent utiliser ou non un certificat client. Les clients sont invités à entrer un certificat par défaut. Par exemple, les clients de navigateur affichent une requête afin de sélectionner un certificat pour l’authentification.
Utilisateur interactif facultatif Les requêtes peuvent utiliser ou non un certificat client. Les clients ne sont pas invités à entrer un certificat par défaut. Par exemple, les clients de navigateur n’affichent pas de requête afin de sélectionner un certificat pour l’authentification.

Pour faire de même avec Azure CLI, exécutez la commande suivante dans le Cloud Shell :

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

Exclure les chemins d’accès nécessitant une authentification

Lorsque vous activez l’authentification mutuelle pour votre application, tous les chemins d’accès situés sous la racine de votre application exigent un certificat client pour y accéder. Pour lever cette exigence pour certains chemins d’accès, définissez des chemins d’exclusion dans le cadre de la configuration de votre application.

  1. Dans la barre de navigation gauche de la page de gestion de votre application, sélectionnez Configuration>Paramètres généraux.

  2. En regard de Chemins d’accès d’exclusion de certificat, sélectionnez l’icône Modifier.

  3. Sélectionnez Nouveau chemin, spécifiez un chemin ou une liste de chemins séparés par , ou ; , puis sélectionnez OK.

  4. En haut de la page, sélectionnez Enregistrer.

Dans la capture d’écran suivante, les chemins pour votre application qui commencent par /public ne nécessitent pas de certificat client. La correspondance de chemins ne respecte pas la casse.

Chemins d'exclusion de certificat

Accéder au certificat client

Dans App Service, l’arrêt TLS de la requête se produit sur l’équilibreur de charge front-end. Lorsque App Service transfère la requête à votre code d’application avec les certificats clients activés, il injecte un en-tête de requête X-ARR-ClientCert avec le certificat client. App Service ne fait rien d’autre avec ce certificat client que de le transférer à votre application. Votre code d’application est responsable de la validation du certificat client.

Pour ASP.NET, le certificat client est disponible via la propriété HttpRequest.ClientCertificate.

Pour les autres piles d’application (Node.js, PHP, etc.), le certificat client est disponible dans votre application via une valeur codée au format base64 dans l’en-tête de requête X-ARR-ClientCert.

Exemple ASP.NET Core

Pour ASP.NET Core, un intergiciel est fourni pour analyser les certificats transférés. Un intergiciel distinct est fourni pour utiliser les en-têtes de protocole transférés. Les deux doivent être présents pour que les certificats transférés soient acceptés. Vous pouvez placer une logique de validation de certificat personnalisée dans les options 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?}");
        });
    }
}

Formulaires WebForms 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 isn't 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 doesn't 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;
            }
        }
    }

Exemple Node.js

L’exemple de code Node.js suivant récupère l’en-tête X-ARR-ClientCert et utilise node-forge pour convertir la chaîne PEM codée au format base64 en objet de certificat, et le valider :

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

Exemple Java

La classe Java suivante encode le certificat en passant d’une instance X-ARR-ClientCert à une instance X509Certificate. certificateIsValid() confirme que l’empreinte numérique du certificat correspond à celle fournie dans le constructeur et que ce certificat n’a pas expiré.

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 hasn't expired.
     * @return True if the certificate's thumbprint matches and hasn't expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

    /**
     * Check certificate's timestamp.
     * @return Returns true if the certificate hasn't 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;
    }
}

Exemple de code Python

Les exemples de code Python Flask et Django suivants implémentent un élément décoratif nommé authorize_certificate qui peut être utilisé sur une fonction d’affichage pour autoriser l’accès uniquement aux appelants qui présentent un certificat client valide. Il attend un certificat au format PEM dans l’en-tête X-ARR-ClientCert et utilise le package de chiffrement Python pour valider le certificat en fonction de son empreinte digitale (empreinte numérique), du nom commun de l’objet, du nom commun de l’émetteur, et des dates de début et d’expiration. Si la validation échoue, l’élément décoratif garantit qu’une réponse HTTP avec le code d’état 403 (Interdit) est retournée au client.

from functools import wraps
from datetime import datetime, timezone
from flask import abort, request
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes


def validate_cert(request):

    try:
        cert_value =  request.headers.get('X-ARR-ClientCert')
        if cert_value is None:
            return False
        
        cert_data = ''.join(['-----BEGIN CERTIFICATE-----\n', cert_value, '\n-----END CERTIFICATE-----\n',])
        cert = x509.load_pem_x509_certificate(cert_data.encode('utf-8'))
    
        fingerprint = cert.fingerprint(hashes.SHA1())
        if fingerprint != b'12345678901234567890':
            return False
        
        subject = cert.subject
        subject_cn = subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        if subject_cn != "contoso.com":
            return False
        
        issuer = cert.issuer
        issuer_cn = issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        if issuer_cn != "contoso.com":
            return False
    
        current_time = datetime.now(timezone.utc)
    
        if current_time < cert.not_valid_before_utc:
            return False
        
        if current_time > cert.not_valid_after_utc:
            return False
        
        return True

    except Exception as e:
        # Handle any errors encountered during validation
        print(f"Encountered the following error during certificate validation: {e}")
        return False
    
def authorize_certificate(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not validate_cert(request):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

L’extrait de code suivant montre comment utiliser l’élément décoratif sur une fonction de vue Flask.

@app.route('/hellocert')
@authorize_certificate
def hellocert():
   print('Request for hellocert page received')
   return render_template('index.html')