次の方法で共有


Azure API Management を使用してシングルページ アプリケーションのアクセス トークンを保護する

Azure API Management
Microsoft Entra ID
Azure Static Web Apps

このガイドでは、Azure API Management を使用して、ブラウザー セッションにトークンを格納しない JavaScript シングルページ アプリケーション用のステートレス アーキテクチャを実装する方法について説明します。 これにより、クロスサイト スクリプティング (XSS) 攻撃からアクセス トークンを保護し、悪意のあるコードがブラウザーで実行されないようにできます。

このアーキテクチャでは、API Management を使用して次の操作を行います。

  • Microsoft Entra ID から OAuth2 アクセス トークンを取得するフロントエンドの バックエンド パターンを実装します。
  • Advanced Encryption Standard AES を使用して、アクセス トークンの暗号化と暗号化解除を行います。
  • トークンを HttpOnly Cookie に格納します。
  • 承認を必要とするすべての API 呼び出しをプロキシします。

バックエンドはトークンの取得を処理するため、シングルページ アプリケーションでは、Microsoft Authentication Library for JavaScript (MSAL.js)など、他のコードやライブラリは必要ありません。 この設計を使用する場合、トークンはブラウザー セッションまたはローカル ストレージに格納されません。 アクセス トークンを暗号化して HttpOnly Cookie に格納すると、XSS 攻撃からアクセス トークン 保護するのに役立ちます。 API ドメインにスコープを設定し、SameSiteStrict に設定すると、プロキシされたすべての API ファースト パーティ要求で Cookie が自動的に送信されます。

建築

ブラウザーにトークンを格納しないアーキテクチャを示す図。

このアーキテクチャの Visio ファイル をダウンロードします。

ワークフロー

  1. ユーザーがシングルページ アプリケーション サインイン を選択します。
  2. シングルページ アプリケーションは、Microsoft Entra 承認エンドポイントへのリダイレクトを介して承認コード フローを呼び出します。
  3. ユーザーは自分自身を認証します。
  4. 承認コードを含む承認コード フロー応答は、API Management コールバック エンドポイントにリダイレクトされます。
  5. API Management ポリシーは、Microsoft Entra トークン エンドポイントを呼び出すことによって、承認コードをアクセス トークンと交換します。
  6. Azure API Management ポリシーは、アプリケーションにリダイレクトし、暗号化されたアクセス トークンを HttpOnly Cookie に配置します。
  7. ユーザーは、API Management プロキシエンドポイントを介してアプリケーションから外部 API 呼び出しを呼び出します。
  8. API Management ポリシーは、API 要求を受信し、Cookie を復号化し、ダウンストリーム API 呼び出しを行い、アクセス トークンを Authorization ヘッダーとして追加します。

コンポーネント

  • Microsoft Entra ID は、Azure ワークロード全体で ID サービス、シングル サインオン、多要素認証を提供します。
  • API Management は、すべての環境にわたる API 用のハイブリッド マルチクラウド管理プラットフォームです。 API Management は、既存のバックエンド サービス用に一貫性のある最新の API ゲートウェイを作成します。
  • Azure Static Web Apps は、コード リポジトリからフル スタック Web アプリを自動的にビルドして Azure にデプロイするサービスです。 デプロイは、GitHub または Azure DevOps リポジトリのアプリケーション ソース コードに加えられた変更によってトリガーされます。

シナリオの詳細

シングルページ アプリケーションは JavaScript で記述され、クライアント側ブラウザーのコンテキストで実行されます。 この実装では、ユーザーはブラウザーで実行されている任意のコードにアクセスできます。 ブラウザーまたは XSS 攻撃で実行されている悪意のあるコードもデータにアクセスする可能性があります。 ブラウザー セッションまたはローカル ストレージに格納されているデータにアクセスできるため、アクセス トークンなどの機密データを使用してユーザーを偽装できます。

ここで説明するアーキテクチャでは、トークンの取得とストレージをバックエンドに移動し、暗号化された HttpOnly Cookie を使用してアクセス トークンを格納することで、アプリケーションのセキュリティが向上します。 アクセス トークンは、ブラウザー セッションまたはローカル ストレージに格納する必要はありません。また、ブラウザーで実行されている悪意のあるコードからアクセスすることはできません。

このアーキテクチャでは、API Management ポリシーはアクセス トークンの取得と Cookie の暗号化と暗号化解除を処理します。 ポリシー は、API の要求または応答で順番に実行され、XML 要素と C# スクリプトで構成されるステートメントのコレクションです。

HttpOnly Cookie に Cookie を格納すると、XSS 攻撃からトークンを保護し、JavaScript からアクセスできないようにすることができます。 Cookie を API ドメインにスコープし、SameSiteStrict に設定すると、プロキシされたすべての API ファースト パーティ要求で Cookie が自動的に送信されます。 この設計により、バックエンドによってシングルページ アプリケーションから行われたすべての API 呼び出しの Authorization ヘッダーにアクセス トークンが自動的に追加されます。

このアーキテクチャでは SameSite=Strict Cookie を使用するため、API Management ゲートウェイのドメインはシングルページ アプリケーションのドメインと同じである必要があります。 これは、API 要求が同じドメイン内のサイトから送信された場合にのみ、Cookie が API Management ゲートウェイに送信されるためです。 ドメインが異なる場合、Cookie は API 要求に追加されず、プロキシされた API 要求は認証されません。

このアーキテクチャは、API Management インスタンスと静的 Web アプリにカスタム ドメインを使用せずに構成できますが、Cookie 設定に SameSite=None を使用する必要があります。 この実装では、API Management ゲートウェイの任意のインスタンスにすべての要求に Cookie が追加されるため、実装の安全性が低下します。 詳細については、「SameSite Cookie する」を参照してください。

Azure リソースにカスタム ドメインを使用する方法の詳細については、「Azure Static Web Apps を使用したカスタム ドメインの」および「Azure API Management インスタンスのカスタム ドメイン名の構成」を参照してください。 カスタム ドメインの DNS レコードの構成の詳細については、「Azure portalで DNS ゾーンを管理する方法」を参照してください。

認証フロー

このプロセスでは、OAuth2 承認コード フローを使用します。 シングルページ アプリケーションが API にアクセスできるようにするアクセス トークンを取得するには、まずユーザーが自分自身を認証する必要があります。 ユーザーを Microsoft Entra 承認エンドポイントにリダイレクトすることで、認証フローを呼び出します。 Microsoft Entra ID でリダイレクト URI を構成する必要があります。 このリダイレクト URI は、API Management コールバック エンドポイントである必要があります。 ユーザーは、Microsoft Entra ID を使用して自分自身を認証するように求められ、承認コードを使用して API Management コールバック エンドポイントにリダイレクトされます。 その後、API Management ポリシーは、Microsoft Entra トークン エンドポイントを呼び出して、アクセス トークンの承認コードを交換します。 次の図は、このフローのイベントのシーケンスを示しています。

認証フローを示す図。

フローには、次の手順が含まれています。

  1. シングルページ アプリケーションが API にアクセスできるようにするためのアクセス トークンを取得するには、まずユーザーが自身を認証する必要があります。 ユーザーは、Microsoft ID プラットフォーム承認エンドポイントにリダイレクトするボタンを選択してフローを呼び出します。 redirect_uri は、API Management ゲートウェイの /auth/callback API エンドポイントに設定されます。

  2. ユーザーは自分自身を認証するように求められます。 認証が成功すると、Microsoft ID プラットフォームはリダイレクトで応答します。

  3. ブラウザーは、API Management コールバック エンドポイントである redirect_uriにリダイレクトされます。 承認コードはコールバック エンドポイントに渡されます。

  4. コールバック エンドポイントの受信ポリシーが呼び出されます。 ポリシーは、Microsoft Entra トークン エンドポイントを呼び出すことによって、承認コードをアクセス トークンと交換します。 クライアント ID、クライアント シークレット、承認コードなど、必要な情報が渡されます。

    <send-request ignore-error="false" timeout="20" response-variable-name="response" mode="new">
     <set-url>https://login.microsoftonline.com/{{tenant-id}}/oauth2/v2.0/token</set-url>
     <set-method>POST</set-method>
     <set-header name="Content-Type" exists-action="override">
         <value>application/x-www-form-urlencoded</value>
     </set-header>
     <set-body>@($"grant_type=authorization_code&code={context.Request.OriginalUrl.Query.GetValueOrDefault("code")}&client_id={{client-id}}&client_secret={{client-secret}}&redirect_uri=https://{context.Request.OriginalUrl.Host}/auth/callback")</set-body>
    </send-request>
    
  5. アクセス トークンが返され、tokenという名前の変数に格納されます。

    <set-variable name="token" value="@((context.Variables.GetValueOrDefault<IResponse>("response")).Body.As<JObject>())" />
    
  6. アクセス トークンは AES 暗号化で暗号化され、cookieという名前の変数に格納されます。

    <set-variable name="cookie" value="@{
        var rng = new RNGCryptoServiceProvider();
        var iv = new byte[16];
        rng.GetBytes(iv);
        byte[] tokenBytes = Encoding.UTF8.GetBytes((string)(context.Variables.GetValueOrDefault<JObject>("token"))["access_token"]);
        byte[] encryptedToken = tokenBytes.Encrypt("Aes", Convert.FromBase64String("{{enc-key}}"), iv);
        byte[] combinedContent = new byte[iv.Length + encryptedToken.Length];
        Array.Copy(iv, 0, combinedContent, 0, iv.Length);
        Array.Copy(encryptedToken, 0, combinedContent, iv.Length, encryptedToken.Length);
        return System.Net.WebUtility.UrlEncode(Convert.ToBase64String(combinedContent));
     }" />
    
  7. シングルページ アプリケーションにリダイレクトするために、コールバック エンドポイントの送信ポリシーが呼び出されます。 HttpOnly に設定され、API Management ゲートウェイのドメインにスコープが設定 SameSiteStrict Cookie に暗号化されたアクセス トークンが設定されます。 明示的な有効期限が設定されていないため、Cookie はセッション Cookie として作成され、ブラウザーが閉じられると有効期限が切れます。

    <return-response>
        <set-status code="302" reason="Temporary Redirect" />
        <set-header name="Set-Cookie" exists-action="override">
            <value>@($"{{cookie-name}}={context.Variables.GetValueOrDefault<string>("cookie")}; Secure; SameSite=Strict; Path=/; Domain={{cookie-domain}}; HttpOnly")</value>
        </set-header>
        <set-header name="Location" exists-action="override">
            <value>{{return-uri}}</value>
        </set-header>
    </return-response>
    

API 呼び出しフロー

シングルページ アプリケーションにアクセス トークンがある場合は、トークンを使用してダウンストリーム API を呼び出すことができます。 Cookie のスコープはシングルページ アプリケーションのドメインであり、SameSite=Strict 属性を使用して構成されているため、要求に自動的に追加されます。 その後、アクセス トークンを復号化して、ダウンストリーム API の呼び出しに使用できます。 次の図は、このフローのイベントのシーケンスを示しています。

API 呼び出しシーケンスを示す図。

フローには、次の手順が含まれています。

  1. ユーザーがシングルページ アプリケーションでボタンを選択して、ダウンストリーム API を呼び出します。 このアクションは、API Management ゲートウェイの /graph/me API エンドポイントを呼び出す JavaScript 関数を呼び出します。

  2. Cookie のスコープはシングルページ アプリケーションのドメインであり、SameSiteに設定 Strict があるため、ブラウザーは API に要求を送信するときに Cookie を自動的に追加します。

  3. API Management ゲートウェイが要求を受信すると、/graph/me エンドポイントの受信ポリシーが呼び出されます。 このポリシーは、Cookie からアクセス トークンを復号化し、access_tokenという名前の変数に格納します。

    <set-variable name="access_token" value="@{
        try {
            string cookie = context.Request.Headers
                .GetValueOrDefault("Cookie")?
                .Split(';')
                .ToList()?
                .Where(p => p.Contains("{{cookie-name}}"))
                .FirstOrDefault()
                .Replace("{{cookie-name}}=", "");
            byte[] encryptedBytes = Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookie));
            byte[] iv = new byte[16];
            byte[] tokenBytes = new byte[encryptedBytes.Length - 16];
            Array.Copy(encryptedBytes, 0, iv, 0, 16);
            Array.Copy(encryptedBytes, 16, tokenBytes, 0, encryptedBytes.Length - 16);
            byte[] decryptedBytes = tokenBytes.Decrypt("Aes", Convert.FromBase64String("{{enc-key}}"), iv);
            char[] convertedBytesToChar = Encoding.UTF8.GetString(decryptedBytes).ToCharArray();
            return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(convertedBytesToChar));
        } catch (Exception ex) {
            return null;
        }
    }" />
    
  4. アクセス トークンは、Authorization ヘッダーとしてダウンストリーム API への要求に追加されます。

    <choose>
        <when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("access_token")))">
            <set-header name="Authorization" exists-action="override">
                <value>@($"Bearer {context.Variables.GetValueOrDefault<string>("access_token")}")</value>
            </set-header>
        </when>
    </choose>
    
  5. 要求は、Authorization ヘッダーに追加されたアクセス トークンを使用してダウンストリーム API にプロキシされます。

  6. ダウンストリーム API からの応答は、シングルページ アプリケーションに直接返されます。

このシナリオをデプロイする

ここで説明するポリシーの完全な例については、OpenAPI 仕様と完全なデプロイ ガイドと共に、この GitHub リポジトリを参照してください。

強化

このソリューションは運用環境に対応していません。 これは、ここで説明するサービスを使用してできることのデモンストレーションを目的としています。 運用環境でソリューションを使用する前に、次の要因を考慮してください。

  • この例では、アクセス トークンの有効期限や、更新トークンまたは ID トークンの使用は実装しません。
  • サンプル内の Cookie の内容は、AES 暗号化を使用して暗号化されます。 キーは、API Management インスタンスの 名前付き値 ペインにシークレットとして格納されます。 この名前付き値をより適切に保護するために、Azure Key Vault に格納されているシークレットへの参照使用できます。 キー管理 ポリシーの一部として、暗号化キーを定期的にローテーションする必要があります。
  • この例では、単一のダウンストリーム API への呼び出しのみをプロキシするため、必要なアクセス トークンは 1 つだけです。 このシナリオでは、ステートレス アプローチを使用できます。 ただし、HTTP Cookie のサイズ制限のため、複数のダウンストリーム API への呼び出しをプロキシする必要がある場合は、ステートフルなアプローチが必要です。 このアプローチでは、単一のアクセス トークンを使用するのではなく、キャッシュにアクセス トークンを格納し、呼び出されている API と Cookie で提供されるキーに基づいてアクセス トークンを取得します。 この方法は、API Management キャッシュ または外部の Redis キャッシュを使用して実装できます。
  • この例では、GET 要求を介してのみデータを取得する方法を示しているため、CSRF 攻撃 対する保護は提供されません。 POST、PUT、PATCH、DELETE などの他の HTTP メソッドを使用する場合は、この保護が必要です。

貢献

この記事は Microsoft によって管理されています。 もともとは次の共同作成者によって作成されました。

プリンシパルの作成者:

その他の共同作成者:

非公開の LinkedIn プロファイルを表示するには、LinkedIn にサインインします。

次の手順