Konfigurera ömsesidig TLS-autentisering för Azure App Service

Du kan begränsa åtkomsten till din Azure App Service-app genom att aktivera olika typer av autentisering för den. Ett sätt att göra det på är att begära ett klientcertifikat när klientbegärandet sker via TLS/SSL och verifiera certifikatet. Den här metoden kallas för ömsesidig TLS-autentisering eller klientcertifikatsautentisering. I artikeln beskrivs hur du konfigurerar din app till att använda autentisering med klientcertifikat.

Kommentar

Din appkod ansvarar för att verifiera klientcertifikatet. App Service gör inget annat med det här klientcertifikatet än att vidarebefordra det till din app.

Om du kommer åt webbplatsen via HTTP och inte HTTPS får du inget klientcertifikat. Så om ditt program kräver klientcertifikat bör du inte tillåta begäranden till ditt program via HTTP.

Förbereda din webbapp

Om du vill skapa anpassade TLS/SSL-bindningar eller aktivera klientcertifikat för din App Service-app måste Din App Service-plan ligga på nivån Basic, Standard, Premium eller Isolerad . Följ dessa steg för att se till att webbappen finns på prisnivån som stöds:

Gå till din webbapp

  1. I sökrutan Azure Portal letar du upp och väljer App Services.

    Skärmbild av Azure Portal, sökrutan och

  2. På sidan App Services väljer du webbappens namn.

    Skärmbild av App Services-sidan i Azure Portal som visar en lista över alla webbappar som körs, med den första appen i listan markerad.

    Nu är du på webbappens hanteringssida.

Kontrollera prisnivån

  1. I den vänstra menyn för webbappen går du till avsnittet Inställningar och väljer Skala upp (App Service-plan).

    Skärmbild av webbappsmenyn, avsnittet Inställningar och

  2. Kontrollera att webbappen inte finns på F1 - eller D1-nivån , som inte stöder anpassad TLS/SSL.

  3. Om du behöver skala upp följer du stegen i nästa avsnitt. Annars stänger du sidan Skala upp och hoppar över avsnittet Skala upp din App Service-plan.

Skala upp App Service-planen

  1. Välj valfri icke-kostnadsfri nivå, till exempel B1, B2, B3 eller någon annan nivå i kategorin Produktion .

  2. När du är klar väljer du Välj.

    När följande meddelande visas har skalningsåtgärden slutförts.

    Skärmbild med bekräftelsemeddelande för uppskalningsåtgärd.

Aktivera klientcertifikat

Så här konfigurerar du din app för att kräva klientcertifikat:

  1. I det vänstra navigeringsfältet på appens hanteringssida väljer du Allmänna inställningar för konfiguration>.

  2. Välj Valfritt klientcertifikatläge . Överst på sidan väljer du Spara.

Klientcertifikatlägen beskrivning
Obligatoriskt Alla begäranden kräver ett klientcertifikat.
Valfritt Begäranden får eller får inte använda ett klientcertifikat. Klienter uppmanas att ange ett certifikat som standard. Webbläsarklienter visar till exempel en uppmaning om att välja ett certifikat för autentisering.
Valfri interaktiv användare Begäranden får eller får inte använda ett klientcertifikat. Klienter uppmanas inte att ange ett certifikat som standard. Webbläsarklienter visar till exempel inte någon uppmaning om att välja ett certifikat för autentisering.

Kör följande kommando i Cloud Shell för att göra samma sak med Azure CLI:

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

Undanta sökvägar från att kräva autentisering

När du aktiverar ömsesidig autentisering för ditt program kräver alla sökvägar under appens rot ett klientcertifikat för åtkomst. Om du vill ta bort det här kravet för vissa sökvägar definierar du undantagssökvägar som en del av programkonfigurationen.

  1. I det vänstra navigeringsfältet på appens hanteringssida väljer du Allmänna inställningar för konfiguration>.

  2. Bredvid Sökvägar för certifikatundantag väljer du redigeringsikonen.

  3. Välj Ny sökväg, ange en sökväg eller en lista med sökvägar avgränsade med , eller ;och välj OK.

  4. Överst på sidan väljer du Spara.

I följande skärmbild begär inte någon sökväg för din app som börjar med /public ett klientcertifikat. Sökvägsmatchning är skiftlägeskänslig.

Sökvägar för certifikatundantag

Åtkomst till klientcertifikat

I App Service sker TLS-avslutningen av begäran i klientdelens lastbalanserare. När App Service vidarebefordrar begäran till din appkod med aktiverade klientcertifikat matas en X-ARR-ClientCert begäranderubrik in med klientcertifikatet. App Service gör inget annat med det här klientcertifikatet än att vidarebefordra det till din app. Din appkod ansvarar för att verifiera klientcertifikatet.

För ASP.NET är klientcertifikatet tillgängligt via egenskapen HttpRequest.ClientCertificate .

För andra programstackar (Node.js, PHP osv.) är klientcertifikatet tillgängligt i din app via ett base64-kodat värde i X-ARR-ClientCert begärandehuvudet.

ASP.NET Core-exempel

För ASP.NET Core tillhandahålls mellanprogram för att parsa vidarebefordrade certifikat. Separata mellanprogram tillhandahålls för att använda de vidarebefordrade protokollrubrikerna. Båda måste finnas för att vidarebefordrade certifikat ska accepteras. Du kan placera anpassad valideringslogik för certifikat i alternativen 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 WebForms-exempel

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

Node.js exempel

Följande Node.js exempelkod hämtar X-ARR-ClientCert huvudet och använder node-forge för att konvertera den base64-kodade PEM-strängen till ett certifikatobjekt och verifiera det:

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-exempel

Följande Java-klass kodar certifikatet från X-ARR-ClientCert till en X509Certificate instans. certificateIsValid() verifierar att certifikatets tumavtryck matchar det som anges i konstruktorn och att certifikatet inte har upphört att gälla.

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

Python-exempel

Följande Flask- och Django Python-kodexempel implementerar en dekoratör med namnet authorize_certificate som kan användas i en vyfunktion för att endast tillåta åtkomst till anropare som presenterar ett giltigt klientcertifikat. Det förväntar sig ett PEM-formaterat certifikat i X-ARR-ClientCert huvudet och använder Python-kryptografipaketet för att verifiera certifikatet baserat på dess fingeravtryck (tumavtryck), ämnesnamn, utfärdarens gemensamma namn och start- och förfallodatum. Om verifieringen misslyckas ser dekoratören till att ett HTTP-svar med statuskod 403 (förbjudet) returneras till klienten.

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

Följande kodfragment visar hur du använder dekoratören på en Flask-vyfunktion.

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