Bagikan melalui


Mengonfigurasi autentikasi bersama TLS untuk Azure App Service

Anda dapat membatasi akses ke aplikasi Azure App Service dengan mengaktifkan berbagai jenis autentikasi untuk aplikasi tersebut. Salah satu cara untuk melakukannya adalah dengan meminta sertifikat klien ketika permintaan klien lebih dari TLS / SSL dan memvalidasi sertifikat. Mekanisme ini disebut autentikasi bersama TLS atau autentikasi sertifikat klien. Artikel ini memperlihatkan cara menyiapkan aplikasi Anda untuk menggunakan autentikasi sertifikat klien.

Catatan

Jika Anda mengakses situs Anda melalui HTTP dan bukan HTTPS, Anda tidak akan menerima sertifikat klien apa pun. Jadi jika aplikasi Anda memerlukan sertifikat klien, Anda tidak boleh mengizinkan permintaan ke aplikasi Anda melalui HTTP.

Mempersiapkan aplikasi web Anda

Untuk membuat pengikatan TLS/SSL kustom atau mengaktifkan sertifikat klien untuk aplikasi App Service Anda, paket App Service Anda harus berada di tingkat Dasar, Standar, Premium, atau Terisolasi. Untuk memastikan bahwa aplikasi web Anda berada di tingkat harga yang didukung, ikuti langkah-langkah berikut:

Buka aplikasi web Anda

  1. Di kotak pencarian portal Microsoft Azure, cari dan pilih App Services.

    Cuplikan layar portal Microsoft Azure, kotak pencarian, dan

  2. Pada halaman App Services, pilih nama aplikasi web Anda.

    Cuplikan layar halaman App Services di portal Azure memperlihatkan daftar semua aplikasi web yang sedang berjalan, dengan aplikasi pertama dalam daftar yang disorot.

    Anda sekarang berada di halaman manajemen aplikasi web Anda.

Periksa tingkat harga

  1. Di menu sebelah kiri untuk aplikasi web Anda, di bagian bawah Pengaturan, pilih Tingkatkan (paket App Service).

    Cuplikan layar menu aplikasi web, bagian

  2. Pastikan aplikasi web Anda tidak berada di tingkat F1 atau D1, yang tidak mendukung TLS/SSL kustom.

  3. Jika Anda perlu meningkatkan skala, ikuti langkah-langkah di bagian berikutnya. Jika tidak, tutup halaman Tingkatkan, dan lewati bagian Tingkatkan paket App Service Anda.

Tingkatkan skala paket App Service Anda

  1. Pilih salah satu tingkat yang tidak gratis (B1, B2, B3, atau tingkat apa pun dalam kategori Produksi).

  2. Setelah selesai, pilih Pilih.

    Ketika pesan berikut muncul, operasi skala telah selesai.

    Cuplikan layar dengan pesan konfirmasi untuk operasi peningkatan.

Aktifkan sertifikat klien

Untuk menyiapkan aplikasi Anda agar memerlukan sertifikat klien:

  1. Dari navigasi kiri halaman manajemen aplikasi Anda, pilih Konfigurasi>Pengaturan Umum.

  2. Atur Mode sertifikat Klien ke Perlu. Pilih Simpan di bagian atas halaman.

Untuk melakukan hal yang sama dengan Azure CLI, jalankan perintah berikut di Cloud Shell:

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

Mengecualikan jalur agar tidak memerlukan autentikasi

Saat Anda mengaktifkan autentikasi mutal untuk aplikasi Anda, semua jalur di bawah root aplikasi Anda memerlukan sertifikat klien untuk akses. Untuk menghapus persyaratan ini untuk jalur tertentu, tentukan jalur pengecualian sebagai bagian dari konfigurasi aplikasi Anda.

  1. Dari navigasi kiri halaman manajemen aplikasi Anda, pilih Konfigurasi>Pengaturan Umum.

  2. Di samping Jalur pengecualian sertifikat, pilih ikon edit.

  3. Pilih Jalur baru, tentukan jalur, atau daftar jalur yang dipisahkan oleh , atau ;, dan pilih OK.

  4. Pilih Simpan di bagian atas halaman.

Dalam cuplikan layar berikut, jalur apa pun untuk aplikasi Anda yang dimulai dengan /public tidak meminta sertifikat klien. Pencocokan jalur tidak peka huruf besar/kecil.

Jalur Pengecualian Sertifikat

Akses sertifikat klien

Di App Service, penghentian TLS permintaan terjadi di frontend load balancer. Saat App Service meneruskan permintaan ke kode aplikasi Anda dengan sertifikat klien diaktifkan, App Service menyuntikkan X-ARR-ClientCert header permintaan dengan sertifikat klien. App Service tidak melakukan apa pun dengan sertifikat klien ini selain meneruskannya ke aplikasi Anda. Kode aplikasi Anda bertanggung jawab untuk memvalidasi sertifikat klien.

Misalnya ASP.NET, sertifikat klien tersedia melalui properti HttpRequest.ClientCertificate.

Untuk tumpukan aplikasi lainnya (Node.js, PHP, dll.), sertifikasi klien tersedia di aplikasi Anda melalui nilai yang dikodekan base64 di header permintaan X-ARR-ClientCert.

sampel ASP.NET Core

Untuk ASP.NET Core, middleware disediakan untuk mengurai sertifikat yang diteruskan. Middleware terpisah disediakan untuk menggunakan header protokol yang diteruskan. Keduanya harus ada agar sertifikat yang diteruskan dapat diterima. Anda dapat menempatkan logika validasi sertifikat kustom di opsi 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?}");
        });
    }
}

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

Sampel Node.js

Sampel kode Node.js berikut mendapatkan header X-ARR-ClientCert dan menggunakan node-forge untuk mengonversi untai (karakter) PEM yang dikodekan base64 menjadi objek sertifikat dan memvalidasinya:

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

Sampel Java

Kelas Java berikut mengkodekan sertifikat dari X-ARR-ClientCert ke instans X509Certificate. certificateIsValid() memvalidasi bahwa thumbprint sertifikat cocok dengan yang diberikan dalam konstruktor dan sertifikat tersebut belum kedaluwarsa.

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

Sampel Python

Sampel kode Flask dan Django Python berikut mengimplementasikan dekorator bernama authorize_certificate yang dapat digunakan pada fungsi tampilan untuk mengizinkan akses hanya kepada pemanggil yang menyajikan sertifikat klien yang valid. Ini mengharapkan sertifikat berformat PEM di X-ARR-ClientCert header dan menggunakan paket kriptografi Python untuk memvalidasi sertifikat berdasarkan sidik jarinya (thumbprint), nama umum subjek, nama umum penerbit, dan tanggal awal dan kedaluwarsa. Jika validasi gagal, dekorator memastikan bahwa respons HTTP dengan kode status 403 (Terlarang) dikembalikan ke klien.

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

Cuplikan kode berikut menunjukkan cara menggunakan dekorator pada fungsi tampilan Flask.

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