Azure App Service に対する TLS 相互認証の構成Configure TLS mutual authentication for Azure App Service

さまざまな種類の認証を有効にすることで、Azure App Service アプリへのアクセスを制限できます。You can restrict access to your Azure App Service app by enabling different types of authentication for it. その方法の 1 つとして、クライアント要求が TLS/SSL を経由するときにクライアント証明書を要求し、その証明書を検証することが挙げられます。One way to do it is to request a client certificate when the client request is over TLS/SSL and validate the certificate. このメカニズムは TLS 相互認証またはクライアント証明書認証と呼ばれます。This mechanism is called TLS mutual authentication or client certificate authentication. この記事では、クライアント証明書認証を使用するようにアプリを設定する方法について説明します。This article shows how to set up your app to use client certificate authentication.

注意

HTTPS ではなく HTTP 経由でサイトにアクセスする場合は、クライアント証明書を受信しません。If you access your site over HTTP and not HTTPS, you will not receive any client certificate. したがって、アプリケーションにクライアント証明書が必要な場合は、HTTP 経由でのアプリケーションへの要求を許可しないでください。So if your application requires client certificates, you should not allow requests to your application over HTTP.

Web アプリの準備Prepare your web app

カスタム セキュリティ バインディングを作成するか、お使いの App Service アプリのクライアント証明書を有効にするには、App Service プランBasicStandardPremium、または Isolated のいずれかのレベルである必要があります。To create custom security bindings or enable client certificates for your App Service app, your App Service plan must be in the Basic, Standard, Premium, or Isolated tier. この手順では、Web アプリが、サポートされている価格レベルであることを確認します。In this step, you make sure that your web app is in the supported pricing tier.

Azure へのサインインSign in to Azure

Azure Portalを開きます。Open the Azure portal.

[App Services] を検索して選択します。Search for and select App Services.

[App Services] を選択します

[App Services] ページで、Web アプリの名前を選択します。On the App Services page, select the name of your web app.

リストの最初のアプリが強調表示された、実行中のすべての Web アプリのリストを示す、Azure portal の [App Services] ページのスクリーンショット。

Web アプリの管理ページが表示されます。You have landed on the management page of your web app.

価格レベルの確認Check the pricing tier

Web アプリ ページの左側のナビゲーションで [設定] セクションまでスクロールし、 [スケール アップ (App Service のプラン)] を選択します。In the left-hand navigation of your web app page, scroll to the Settings section and select Scale up (App Service plan).

スケール アップ メニュー

Web アプリが F1 レベルまたは D1 レベルに含まれていないことを確認します。Check to make sure that your web app is not in the F1 or D1 tier. Web アプリの現在のレベルは、ダーク ブルーのボックスで強調表示されます。Your web app's current tier is highlighted by a dark blue box.

価格レベルの確認

カスタム SSL は、F1 レベルまたは D1 レベルではサポートされていません。Custom SSL is not supported in the F1 or D1 tier. スケール アップする必要がある場合は、次のセクションの手順に従います。If you need to scale up, follow the steps in the next section. それ以外の場合は、 [スケール アップ] ページを閉じて、「App Service プランのスケール アップ」セクションはスキップしてください。Otherwise, close the Scale up page and skip the Scale up your App Service plan section.

App Service プランのスケール アップScale up your App Service plan

非 Free レベルのいずれかを選びます (B1B2B3、または運用カテゴリのいずれかのレベル)。Select any of the non-free tiers (B1, B2, B3, or any tier in the Production category). その他のオプションについては、 [See additional options](その他のオプションを参照する) をクリックします。For additional options, click See additional options.

[Apply] をクリックします。Click Apply.

価格レベルの選択

次の通知が表示されたら、スケール操作は完了です。When you see the following notification, the scale operation is complete.

スケール アップの通知

クライアント証明書を有効にするEnable client certificates

アプリでクライアント証明書を必須にするようアプリを設定するには、Azure Portal で [構成] > [全般設定] の順に選択して、[Require incoming certificate](着信証明書を必須にする) を [オン] にするか、アプリの clientCertEnabled 設定を true に設定する必要があります。To set up your app to require client certificates, you can switch On the Require incoming certificate by selecting Configuration > General Settings from the Azure Portal or you need to set the clientCertEnabled setting for your app to true. この設定を行うには、Cloud Shell で次のコマンドを実行します。To set the setting, run the following command in the Cloud Shell.

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

パスを認証を必要としないものとして除外するExclude paths from requiring authentication

お使いのアプリケーションで相互認証を有効にすると、お使いのアプリのルート下のすべてのパスで、アクセスにクライアント証明書が必要になります。When you enable mutual auth for your application, all paths under the root of your app will require a client certificate for access. 特定のパスが匿名アクセスできるよう残すには、お使いのアプリケーションを構成するときに除外するパスを定義する必要があります。To allow certain paths to remain open for anonymous access, you can define exclusion paths as part of your application configuration.

除外するパスを構成するには、 [構成] > [全般設定] の順に選択して除外するパスを定義します。Exclusion paths can be configured by selecting Configuration > General Settings and defining an exclusion path. この例では、お使いのアプリケーションの /public パスの下のすべてで、クライアント証明書は要求されません。In this example, anything under /public path for your application would not request a client certificate.

証明書不要のパス

クライアント証明書にアクセスするAccess client certificate

App Service では、要求の TLS 終了がフロントエンドのロード バランサー側で行われます。In App Service, TLS termination of the request happens at the frontend load balancer. クライアント証明書を有効にした状態で要求をアプリ コードに転送すると、App Service によって X-ARR-ClientCert 要求ヘッダーにクライアント証明書が挿入されます。When forwarding the request to your app code with client certificates enabled, App Service injects an X-ARR-ClientCert request header with the client certificate. App Service がこのクライアント証明書に対して行うのは、この証明書をアプリに転送する処理だけです。App Service does not do anything with this client certificate other than forwarding it to your app. クライアント証明書の検証はアプリ コードが行います。Your app code is responsible for validating the client certificate.

ASP.NET の場合は、HttpRequest.ClientCertificate プロパティを通じてクライアント証明書を使用できます。For ASP.NET, the client certificate is available through the HttpRequest.ClientCertificate property.

他のアプリケーション スタック (Node.js や PHP など) の場合は、X-ARR-ClientCert 要求ヘッダー内の base64 エンコード値を通じて、アプリでクライアント証明書を使用できます。For other application stacks (Node.js, PHP, etc.), the client cert is available in your app through a base64 encoded value in the X-ARR-ClientCert request header.

ASP.NET のサンプルASP.NET sample

    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 sample

次の Node.js サンプル コードは、X-ARR-ClientCert ヘッダーを取得し、node-forge を使用して base64 エンコード PEM 文字列を証明書オブジェクトに変換して検証します。The following Node.js sample code gets the X-ARR-ClientCert header and uses node-forge to convert the base64-encoded PEM string into a certificate object and validate it:

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 sample

次の Java クラスは、X-ARR-ClientCert から X509Certificate インスタンスに証明書をエンコードします。The following Java class encodes the certificate from X-ARR-ClientCert to an X509Certificate instance. certificateIsValid() は、証明書の拇印が、コンストラクターで指定されたものと一致し、その証明書の有効期限が切れていないことを検証します。certificateIsValid() validates that the certificate's thumbprint matches the one given in the constructor and that certificate has not expired.

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