Azure API Management を使用してシングルページ アプリケーションのアクセス トークンを保護する
このガイドでは、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 ドメインにスコープを設定し、SameSite
を Strict
に設定すると、プロキシされたすべての API ファースト パーティ要求で Cookie が自動的に送信されます。
建築
このアーキテクチャの Visio ファイル をダウンロードします。
ワークフロー
- ユーザーがシングルページ アプリケーション サインイン を選択します。
- シングルページ アプリケーションは、Microsoft Entra 承認エンドポイントへのリダイレクトを介して承認コード フローを呼び出します。
- ユーザーは自分自身を認証します。
- 承認コードを含む承認コード フロー応答は、API Management コールバック エンドポイントにリダイレクトされます。
- API Management ポリシーは、Microsoft Entra トークン エンドポイントを呼び出すことによって、承認コードをアクセス トークンと交換します。
- Azure API Management ポリシーは、アプリケーションにリダイレクトし、暗号化されたアクセス トークンを
HttpOnly
Cookie に配置します。 - ユーザーは、API Management プロキシエンドポイントを介してアプリケーションから外部 API 呼び出しを呼び出します。
- 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 ドメインにスコープし、SameSite
を Strict
に設定すると、プロキシされたすべての 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 トークン エンドポイントを呼び出して、アクセス トークンの承認コードを交換します。 次の図は、このフローのイベントのシーケンスを示しています。
フローには、次の手順が含まれています。
シングルページ アプリケーションが API にアクセスできるようにするためのアクセス トークンを取得するには、まずユーザーが自身を認証する必要があります。 ユーザーは、Microsoft ID プラットフォーム承認エンドポイントにリダイレクトするボタンを選択してフローを呼び出します。
redirect_uri
は、API Management ゲートウェイの/auth/callback
API エンドポイントに設定されます。ユーザーは自分自身を認証するように求められます。 認証が成功すると、Microsoft ID プラットフォームはリダイレクトで応答します。
ブラウザーは、API Management コールバック エンドポイントである
redirect_uri
にリダイレクトされます。 承認コードはコールバック エンドポイントに渡されます。コールバック エンドポイントの受信ポリシーが呼び出されます。 ポリシーは、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>
アクセス トークンが返され、
token
という名前の変数に格納されます。<set-variable name="token" value="@((context.Variables.GetValueOrDefault<IResponse>("response")).Body.As<JObject>())" />
アクセス トークンは 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)); }" />
シングルページ アプリケーションにリダイレクトするために、コールバック エンドポイントの送信ポリシーが呼び出されます。
HttpOnly
に設定され、API Management ゲートウェイのドメインにスコープが設定SameSite
、Strict
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 を呼び出します。 このアクションは、API Management ゲートウェイの
/graph/me
API エンドポイントを呼び出す JavaScript 関数を呼び出します。Cookie のスコープはシングルページ アプリケーションのドメインであり、
SameSite
に設定Strict
があるため、ブラウザーは API に要求を送信するときに Cookie を自動的に追加します。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; } }" />
アクセス トークンは、
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>
要求は、
Authorization
ヘッダーに追加されたアクセス トークンを使用してダウンストリーム API にプロキシされます。ダウンストリーム 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 にサインインします。
次の手順
- 実装とデプロイの例に関するガイド
- Azure API Management の ポリシー
- Azure API Management ポリシーを設定または編集する方法
- Azure API Management ポリシーで名前付き値を使用する
- Microsoft Entra ID を使用した OAuth 2.0 認証の
- Azure Static Web Apps とは
関連リソース
- Application Gateway と API Management を使用して API を保護する